@payments-ai/mcp 2.5.0 → 2.7.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.
Files changed (2) hide show
  1. package/dist/main.http.js +360 -167
  2. package/package.json +1 -1
package/dist/main.http.js CHANGED
@@ -249,8 +249,10 @@ var SUPPORTED_OAUTH_SCOPES = [
249
249
  "checkout:write",
250
250
  "transaction:read",
251
251
  "customer:read",
252
- "invoice:read"
252
+ "invoice:read",
253
+ "webhook:listen"
253
254
  ];
255
+ var CLI_EMAIL_CREDENTIAL_SCOPES = [...SUPPORTED_OAUTH_SCOPES];
254
256
  var OAUTH_CONSENT_READ_SCOPES = [
255
257
  "merchant:read",
256
258
  "product:read",
@@ -263,7 +265,8 @@ var OAUTH_CONSENT_READ_SCOPES = [
263
265
  var EXPLICIT_GRANT_OAUTH_SCOPES = [
264
266
  "transaction:read",
265
267
  "customer:read",
266
- "invoice:read"
268
+ "invoice:read",
269
+ "webhook:listen"
267
270
  ];
268
271
  var EXPLICIT_GRANT_SCOPE_SET = new Set(EXPLICIT_GRANT_OAUTH_SCOPES);
269
272
  function isExplicitGrantScope(scope) {
@@ -271,7 +274,11 @@ function isExplicitGrantScope(scope) {
271
274
  }
272
275
  var DEFAULT_OAUTH_SCOPES = OAUTH_CONSENT_READ_SCOPES.filter((scope) => !isExplicitGrantScope(scope));
273
276
  var OAUTH_RESPONSE_TYPES_SUPPORTED = ["code"];
274
- var OAUTH_GRANT_TYPES_SUPPORTED = ["authorization_code"];
277
+ var OAUTH_DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
278
+ var OAUTH_GRANT_TYPES_SUPPORTED = [
279
+ "authorization_code",
280
+ OAUTH_DEVICE_CODE_GRANT_TYPE
281
+ ];
275
282
  var OAUTH_CODE_CHALLENGE_METHODS_SUPPORTED = ["S256"];
276
283
  function buildOAuthAuthorizationServerMetadata(issuerUrl, options2) {
277
284
  return {
@@ -280,6 +287,7 @@ function buildOAuthAuthorizationServerMetadata(issuerUrl, options2) {
280
287
  token_endpoint: `${issuerUrl}/api/v1/oauth/token`,
281
288
  revocation_endpoint: `${issuerUrl}/api/v1/oauth/revoke`,
282
289
  registration_endpoint: `${issuerUrl}/api/v1/oauth/register`,
290
+ device_authorization_endpoint: `${issuerUrl}/api/v1/oauth/device_authorization`,
283
291
  scopes_supported: SUPPORTED_OAUTH_SCOPES,
284
292
  response_types_supported: OAUTH_RESPONSE_TYPES_SUPPORTED,
285
293
  grant_types_supported: OAUTH_GRANT_TYPES_SUPPORTED,
@@ -293,12 +301,67 @@ function buildOAuthAuthorizationServerMetadata(issuerUrl, options2) {
293
301
  var MCP_OAUTH_AUTHORIZATION_WINDOW_MINUTES = 30;
294
302
  var MCP_OAUTH_AUTHORIZATION_WINDOW_MS = MCP_OAUTH_AUTHORIZATION_WINDOW_MINUTES * 60 * 1e3;
295
303
 
304
+ // ../../packages/mcp-contracts/dist/device-user-code.js
305
+ var DEVICE_USER_CODE_ALPHABET = "BCDFGHJKLMNPQRSTVWXZ";
306
+ var DEVICE_USER_CODE_LENGTH = 8;
307
+ var DEVICE_USER_CODE_PATTERN = new RegExp(`^[${DEVICE_USER_CODE_ALPHABET}]{${DEVICE_USER_CODE_LENGTH}}$`);
308
+
296
309
  // ../../packages/mcp-contracts/dist/order-limits.js
297
310
  var import_currency2 = __toESM(require_currency(), 1);
298
311
 
299
312
  // ../../packages/mcp-contracts/dist/currency-precision.js
300
313
  var import_currency = __toESM(require_currency(), 1);
301
314
 
315
+ // ../../packages/mcp-contracts/dist/precision-messages.js
316
+ function formatAmountPrecisionMessage(currency5, fractionDigits) {
317
+ const code = currency5.trim().toUpperCase();
318
+ if (fractionDigits === 0) {
319
+ return `Amount must be a whole number of ${code}, which has no minor unit. Recovery: round the amount to whole ${code}, e.g. 500 rather than 500.5.`;
320
+ }
321
+ return `Amount must have at most ${fractionDigits} decimal places in ${code}. Recovery: round the amount to ${fractionDigits} decimal places.`;
322
+ }
323
+
324
+ // ../../packages/mcp-contracts/dist/currency-precision.js
325
+ function currencyFractionDigits(currencyCode3) {
326
+ const code = currencyCode3.trim().toUpperCase();
327
+ let digits;
328
+ try {
329
+ digits = new Intl.NumberFormat("en-US", {
330
+ style: "currency",
331
+ currency: code
332
+ }).resolvedOptions().maximumFractionDigits;
333
+ } catch {
334
+ throw new Error(`Unsupported currency "${currencyCode3}"`);
335
+ }
336
+ if (typeof digits !== "number") {
337
+ throw new Error(`Unsupported currency "${currencyCode3}"`);
338
+ }
339
+ return digits;
340
+ }
341
+ var MINOR_UNIT_RELATIVE_TOLERANCE = Number.EPSILON * 16;
342
+ function amountExceedsCurrencyPrecision(amount, currencyCode3) {
343
+ const minorUnits = amount * 10 ** currencyFractionDigits(currencyCode3);
344
+ const drift = Math.abs(minorUnits - Math.round(minorUnits));
345
+ return drift > Math.abs(minorUnits) * MINOR_UNIT_RELATIVE_TOLERANCE;
346
+ }
347
+ function describeAmountPrecisionViolation(amount, currencyCode3) {
348
+ const fractionDigits = resolveFractionDigits(currencyCode3);
349
+ if (fractionDigits === void 0 || !amountExceedsCurrencyPrecision(amount, currencyCode3)) {
350
+ return void 0;
351
+ }
352
+ return formatAmountPrecisionMessage(currencyCode3, fractionDigits);
353
+ }
354
+ function describeRequestAmountPrecisionViolation(amount, currencyCode3) {
355
+ return describeAmountPrecisionViolation((0, import_currency.default)(amount).value, currencyCode3);
356
+ }
357
+ function resolveFractionDigits(currencyCode3) {
358
+ try {
359
+ return currencyFractionDigits(currencyCode3);
360
+ } catch {
361
+ return void 0;
362
+ }
363
+ }
364
+
302
365
  // ../../packages/mcp-contracts/dist/plan-amount-limits.js
303
366
  var DEFAULT_MIN_PAID_PLAN_AMOUNT = 1;
304
367
  var MIN_PAID_PLAN_AMOUNT_BY_CURRENCY = {
@@ -331,13 +394,6 @@ function formatMinPaidPlanAmountMessage(currency5, minAmount) {
331
394
  const code = currency5.trim().toUpperCase() || "USD";
332
395
  return `Amount must be at least ${minAmount} ${code} for paid plans`;
333
396
  }
334
- function formatAmountPrecisionMessage(currency5, fractionDigits) {
335
- const code = currency5.trim().toUpperCase();
336
- if (fractionDigits === 0) {
337
- return `Amount must be a whole number of ${code}, which has no minor unit. Recovery: round the amount to whole ${code}, e.g. 500 rather than 500.5.`;
338
- }
339
- return `Amount must have at most ${fractionDigits} decimal places in ${code}. Recovery: round the amount to ${fractionDigits} decimal places.`;
340
- }
341
397
  var LOWEST_MIN_PAID_PLAN_AMOUNT = Math.min(...Object.values(MIN_PAID_PLAN_AMOUNT_BY_CURRENCY));
342
398
  var PAID_PLAN_NOT_FREE_BY_ZERO_AMOUNT = "A paid plan cannot be made free by setting its amount to 0; create a free-access plan instead.";
343
399
  function formatAmountBelowLowestFloorMessage() {
@@ -388,47 +444,6 @@ function formatPeriodLengthRecovery(maxLength) {
388
444
  }
389
445
  var MAX_FREE_TRIAL_DAYS = 730;
390
446
 
391
- // ../../packages/mcp-contracts/dist/currency-precision.js
392
- function currencyFractionDigits(currencyCode3) {
393
- const code = currencyCode3.trim().toUpperCase();
394
- let digits;
395
- try {
396
- digits = new Intl.NumberFormat("en-US", {
397
- style: "currency",
398
- currency: code
399
- }).resolvedOptions().maximumFractionDigits;
400
- } catch {
401
- throw new Error(`Unsupported currency "${currencyCode3}"`);
402
- }
403
- if (typeof digits !== "number") {
404
- throw new Error(`Unsupported currency "${currencyCode3}"`);
405
- }
406
- return digits;
407
- }
408
- var MINOR_UNIT_RELATIVE_TOLERANCE = Number.EPSILON * 16;
409
- function amountExceedsCurrencyPrecision(amount, currencyCode3) {
410
- const minorUnits = amount * 10 ** currencyFractionDigits(currencyCode3);
411
- const drift = Math.abs(minorUnits - Math.round(minorUnits));
412
- return drift > Math.abs(minorUnits) * MINOR_UNIT_RELATIVE_TOLERANCE;
413
- }
414
- function describeAmountPrecisionViolation(amount, currencyCode3) {
415
- const fractionDigits = resolveFractionDigits(currencyCode3);
416
- if (fractionDigits === void 0 || !amountExceedsCurrencyPrecision(amount, currencyCode3)) {
417
- return void 0;
418
- }
419
- return formatAmountPrecisionMessage(currencyCode3, fractionDigits);
420
- }
421
- function describeRequestAmountPrecisionViolation(amount, currencyCode3) {
422
- return describeAmountPrecisionViolation((0, import_currency.default)(amount).value, currencyCode3);
423
- }
424
- function resolveFractionDigits(currencyCode3) {
425
- try {
426
- return currencyFractionDigits(currencyCode3);
427
- } catch {
428
- return void 0;
429
- }
430
- }
431
-
432
447
  // ../../packages/mcp-contracts/dist/order-limits.js
433
448
  function getMaxOrderAmount(currency5) {
434
449
  return getMaxPlanAmount(currency5);
@@ -11258,8 +11273,8 @@ var error43 = () => {
11258
11273
  case "not_multiple_of":
11259
11274
  return `N\xFAmero inv\xE1lido: deve ser m\xFAltiplo de ${issue2.divisor}`;
11260
11275
  case "unrecognized_keys": {
11261
- const plural2 = issue2.keys.length > 1 ? "s" : "";
11262
- return `Chave${plural2} inv\xE1lida${plural2}: ${joinValues(issue2.keys, ", ")}`;
11276
+ const plural3 = issue2.keys.length > 1 ? "s" : "";
11277
+ return `Chave${plural3} inv\xE1lida${plural3}: ${joinValues(issue2.keys, ", ")}`;
11263
11278
  }
11264
11279
  case "invalid_key":
11265
11280
  return `Entrada inv\xE1lida n${translateOriginWithArticle(issue2.origin, "definite")}`;
@@ -11403,8 +11418,8 @@ var error44 = () => {
11403
11418
  case "not_multiple_of":
11404
11419
  return `N\xFAmero inv\xE1lido: deve ser m\xFAltiplo de ${issue2.divisor}`;
11405
11420
  case "unrecognized_keys": {
11406
- const plural2 = issue2.keys.length > 1 ? "s" : "";
11407
- return `Chave${plural2} inv\xE1lida${plural2}: ${joinValues(issue2.keys, ", ")}`;
11421
+ const plural3 = issue2.keys.length > 1 ? "s" : "";
11422
+ return `Chave${plural3} inv\xE1lida${plural3}: ${joinValues(issue2.keys, ", ")}`;
11408
11423
  }
11409
11424
  case "invalid_key":
11410
11425
  return `Entrada inv\xE1lida n${translateOriginWithArticle(issue2.origin, "definite")}`;
@@ -20280,6 +20295,9 @@ var mcpPlanSummarySchema = external_exports.object({
20280
20295
  // ../../packages/mcp-contracts/dist/create-order.js
20281
20296
  var MAX_ORDER_ITEMS = 20;
20282
20297
  var MAX_IDEMPOTENCY_KEY_LENGTH = 255;
20298
+ var MAX_ORDER_METADATA_KEYS = 50;
20299
+ var MAX_ORDER_METADATA_KEY_LENGTH = 40;
20300
+ var MAX_ORDER_METADATA_VALUE_LENGTH = 500;
20283
20301
  var ORDER_ITEM_TYPES = ["one_time", "recurring"];
20284
20302
  var ORDER_BILLING_PERIODS = ["day", "week", "month", "year"];
20285
20303
  var orderQuantitySchema = external_exports.number().int().positive().max(MAX_ORDER_ITEM_QUANTITY, ORDER_QUANTITY_TOO_HIGH_MESSAGE);
@@ -20300,11 +20318,18 @@ var createOrderItemSchema = external_exports.union([
20300
20318
  createOrderPlanItemSchema,
20301
20319
  createOrderCustomItemSchema
20302
20320
  ]);
20321
+ var orderMetadataSchema = external_exports.record(external_exports.string().min(1, "metadata keys must not be empty.").max(MAX_ORDER_METADATA_KEY_LENGTH, `metadata keys must be at most ${MAX_ORDER_METADATA_KEY_LENGTH} characters. Recovery: send a shorter key.`), external_exports.string().max(MAX_ORDER_METADATA_VALUE_LENGTH, `metadata values must be at most ${MAX_ORDER_METADATA_VALUE_LENGTH} characters. Recovery: send a shorter value.`)).refine((metadata) => Object.keys(metadata).length <= MAX_ORDER_METADATA_KEYS, {
20322
+ message: `metadata must have at most ${MAX_ORDER_METADATA_KEYS} keys. Recovery: send fewer keys.`
20323
+ }).meta({
20324
+ maxProperties: MAX_ORDER_METADATA_KEYS,
20325
+ description: 'Your own key/value strings stored on the order and returned exactly as sent, for example { "cf_order_id": "A-1001" }. Not shown to the buyer.'
20326
+ });
20303
20327
  var createOrderInputSchema = external_exports.object({
20304
20328
  merchantId: external_exports.uuid(),
20305
20329
  currency: supportedCurrencyCodeSchema,
20306
20330
  items: external_exports.array(createOrderItemSchema).min(1).max(MAX_ORDER_ITEMS),
20307
- idempotencyKey: external_exports.string().trim().min(1).max(MAX_IDEMPOTENCY_KEY_LENGTH).optional()
20331
+ idempotencyKey: external_exports.string().trim().min(1).max(MAX_IDEMPOTENCY_KEY_LENGTH).optional(),
20332
+ metadata: orderMetadataSchema.optional()
20308
20333
  }).strict().superRefine((input2, ctx) => {
20309
20334
  checkCustomOrderItemAmounts(input2, ctx);
20310
20335
  checkCustomOrderItemsTotal(input2, ctx);
@@ -20327,7 +20352,8 @@ var createOrderOutputSchema = external_exports.object({
20327
20352
  billingPeriod: external_exports.enum(ORDER_BILLING_PERIODS).nullable(),
20328
20353
  periodLength: external_exports.number().int().nullable(),
20329
20354
  trialPeriodDays: external_exports.number().int().nullable(),
20330
- items: external_exports.array(createOrderItemResponseSchema)
20355
+ items: external_exports.array(createOrderItemResponseSchema),
20356
+ metadata: external_exports.record(external_exports.string(), external_exports.string()).default({})
20331
20357
  });
20332
20358
 
20333
20359
  // ../../packages/mcp-contracts/dist/product-name.js
@@ -20591,7 +20617,7 @@ function canonicalResourceUrl(value) {
20591
20617
  }
20592
20618
 
20593
20619
  // ../../packages/mcp-contracts/dist/mcp-session-expired.js
20594
- var MCP_SESSION_EXPIRED_MESSAGE = "MCP Session Expired. Please generate a new token in Developer Tools \u2192 MCP Tokens.";
20620
+ var MCP_SESSION_EXPIRED_MESSAGE = "MCP Session Expired. Re-approve the connection in your AI client, or generate a new token in Developer Tools \u2192 MCP Tokens if you connected with a token.";
20595
20621
 
20596
20622
  // ../../packages/mcp-contracts/dist/list-products.js
20597
20623
  var PLAN_TYPES3 = ["free-access", "one-time", "recurring"];
@@ -20698,13 +20724,32 @@ var mcpMerchantSummarySchema = external_exports.object({
20698
20724
  name: external_exports.string()
20699
20725
  });
20700
20726
 
20727
+ // ../../packages/mcp-contracts/dist/credential-expiry.js
20728
+ var CREDENTIAL_EXPIRY_WARNING_WINDOW_DAYS = 14;
20729
+ var CREDENTIAL_EXPIRY_WARNING_WINDOW_SECONDS = CREDENTIAL_EXPIRY_WARNING_WINDOW_DAYS * 24 * 60 * 60;
20730
+ var CREDENTIAL_EXPIRY_WARNING_META_KEY = "ai.payments/credentialExpiryWarning";
20731
+ var credentialExpiryWarningSchema = external_exports.object({
20732
+ credentialExpiresAt: external_exports.iso.datetime().describe("When the MCP credential stops working (ISO 8601, UTC)."),
20733
+ credentialExpiresInSeconds: external_exports.number().int().nonnegative().describe("Seconds of validity left on the MCP credential; 0 once it has expired."),
20734
+ reapproveUrl: external_exports.url().describe("Page that walks the user through re-approving this MCP connection."),
20735
+ message: external_exports.string().describe("One sentence to relay to the user, with the re-approve link.")
20736
+ });
20737
+
20701
20738
  // ../../packages/mcp-contracts/dist/whoami.js
20702
20739
  var mcpConnectionHostSchema = external_exports.enum(["sandbox", "live"]);
20740
+ var EMBEDDED_EMAIL_ADDRESS_PATTERN = /[^\s@]+@[^\s@]+\.[^\s@]+/;
20741
+ var MCP_ACCOUNT_NAME_MAX_LENGTH = 255;
20742
+ var mcpAccountNameSchema = external_exports.string().min(1).max(MCP_ACCOUNT_NAME_MAX_LENGTH).refine((value) => value.length <= MCP_ACCOUNT_NAME_MAX_LENGTH && !EMBEDDED_EMAIL_ADDRESS_PATTERN.test(value), { message: "An account name must not contain an email address" });
20703
20743
  var mcpConnectionProofSchema = external_exports.object({
20704
20744
  userId: external_exports.uuid(),
20705
20745
  scopes: external_exports.array(external_exports.string()).nullable(),
20706
20746
  host: mcpConnectionHostSchema.describe(`The class of MCP URL this client is connected to: "live" for the main MCP URL, "sandbox" for the sandbox-only MCP URL. Not the environment (dev, stage or prod) and not a merchant's deployment.`),
20707
- endpoint: external_exports.url()
20747
+ endpoint: external_exports.url(),
20748
+ accountName: mcpAccountNameSchema.nullable().describe("The business name of the account you are connected to, never an email address. Quote it with host (live or sandbox) and ask the developer to confirm it is their business; null when the account has no business yet."),
20749
+ merchantIds: external_exports.array(external_exports.uuid()).describe("Ids of the merchants this credential is a member of, as returned when the credential was validated. Call get_my_merchant for names and per-deployment state; on the main MCP URL a merchant that exists on sandbox only is listed there, not here."),
20750
+ credentialExpiresAt: external_exports.iso.datetime().nullable().describe("When this MCP credential stops working (ISO 8601, UTC); null when unknown."),
20751
+ credentialExpiresInSeconds: external_exports.number().int().nonnegative().nullable().describe("Seconds of validity left on this MCP credential; null when the expiry is unknown."),
20752
+ credentialExpiryWarning: credentialExpiryWarningSchema.optional().describe("Present only when the credential expires within the warning window: tell the user and share the re-approve link.")
20708
20753
  });
20709
20754
  var whoamiOutputSchema = mcpConnectionProofSchema;
20710
20755
 
@@ -23018,6 +23063,12 @@ function parsePhoneNumber2(text, options2, metadata) {
23018
23063
  }
23019
23064
  }
23020
23065
 
23066
+ // ../../node_modules/.pnpm/libphonenumber-js@1.13.14/node_modules/libphonenumber-js/es6/parsePhoneNumber.js
23067
+ function parsePhoneNumber3() {
23068
+ var _normalizeArguments = normalizeArguments(arguments), text = _normalizeArguments.text, options2 = _normalizeArguments.options, metadata = _normalizeArguments.metadata;
23069
+ return parsePhoneNumber2(text, options2, metadata);
23070
+ }
23071
+
23021
23072
  // ../../node_modules/.pnpm/libphonenumber-js@1.13.14/node_modules/libphonenumber-js/es6/isValidPhoneNumber.js
23022
23073
  function _typeof7(o) {
23023
23074
  "@babel/helpers - typeof";
@@ -23074,6 +23125,11 @@ function isValidPhoneNumber() {
23074
23125
  return phoneNumber && phoneNumber.isValid() || false;
23075
23126
  }
23076
23127
 
23128
+ // ../../node_modules/.pnpm/libphonenumber-js@1.13.14/node_modules/libphonenumber-js/min/exports/parsePhoneNumber.js
23129
+ function parsePhoneNumber4() {
23130
+ return withMetadataArgument(parsePhoneNumber3, arguments);
23131
+ }
23132
+
23077
23133
  // ../../node_modules/.pnpm/libphonenumber-js@1.13.14/node_modules/libphonenumber-js/min/exports/isValidPhoneNumber.js
23078
23134
  function isValidPhoneNumber2() {
23079
23135
  return withMetadataArgument(isValidPhoneNumber, arguments);
@@ -23082,11 +23138,21 @@ function isValidPhoneNumber2() {
23082
23138
  // ../../packages/mcp-contracts/dist/merchant-phone.js
23083
23139
  var E164_PATTERN = /^\+[1-9]\d{1,14}$/;
23084
23140
  var NANP_FICTIONAL_BLOCK = /^\+1\d{3}55501\d{2}$/;
23141
+ function isDialableWithPlusPrefix(phone) {
23142
+ return /^\d+$/.test(phone) && isDialablePhoneNumber(`+${phone}`);
23143
+ }
23144
+ function countryOfPlusPrefixedDigits(phone) {
23145
+ if (!isDialableWithPlusPrefix(phone))
23146
+ return void 0;
23147
+ return parsePhoneNumber4(`+${phone}`)?.country;
23148
+ }
23085
23149
  function isDialablePhoneNumber(phone) {
23086
23150
  return E164_PATTERN.test(phone) && isValidPhoneNumber2(phone) && !NANP_FICTIONAL_BLOCK.test(phone);
23087
23151
  }
23088
23152
 
23089
23153
  // ../../packages/mcp-contracts/dist/create-merchant-account.js
23154
+ var PHONE_INVALID_MESSAGE = "Provide the user's own mobile number, dialable and in E.164 format with country code (format only: +14155552671). Recovery: ask the user for their real mobile number and never invent a placeholder or a reserved fictional number (a +1 555 area code or a 555-01XX number) \u2014 the identity-verification link is sent to it by SMS after go_live. If the user does not want to share one, omit phone.";
23155
+ var phoneMissingPlusMessage = (phone, country) => `The phone is missing the leading + of E.164 format. With a + it would read +${phone}${country ? ` (${country})` : ""}, which is only right if the number already starts with the user's country code. Recovery: prefix + (for example +14155552671) and re-send only if the number already starts with the user's country code; otherwise ask the user for their number with its country code.`;
23090
23156
  var createMerchantAccountInputSchema = external_exports.object({
23091
23157
  name: external_exports.string().trim().min(1, {
23092
23158
  message: "Provide a non-empty business or display name. Recovery: pass a non-empty string in the name field and re-send the tool call. Merchants are identified downstream by merchantId, not by name, so disambiguate look-alike names by merchantId instead of by picking a distinct name here."
@@ -23099,8 +23165,13 @@ var createMerchantAccountInputSchema = external_exports.object({
23099
23165
  * it by SMS, so shape alone is not enough: an invented placeholder passes E.164 and the
23100
23166
  * SMS silently goes nowhere.
23101
23167
  */
23102
- phone: external_exports.string().trim().refine(isDialablePhoneNumber, {
23103
- message: "Provide the user's own mobile number, dialable and in E.164 format with country code (format only: +14155552671). Recovery: ask the user for their real mobile number and never invent a placeholder or a reserved fictional number (a +1 555 area code or a 555-01XX number) \u2014 the identity-verification link is sent to it by SMS after go_live. If the user does not want to share one, omit phone."
23168
+ phone: external_exports.string().trim().superRefine((phone, ctx) => {
23169
+ if (isDialablePhoneNumber(phone))
23170
+ return;
23171
+ ctx.addIssue({
23172
+ code: "custom",
23173
+ message: isDialableWithPlusPrefix(phone) ? phoneMissingPlusMessage(phone, countryOfPlusPrefixedDigits(phone)) : PHONE_INVALID_MESSAGE
23174
+ });
23104
23175
  }).optional(),
23105
23176
  /**
23106
23177
  * Optional retry correlation id from a prior partial failure.
@@ -25438,6 +25509,7 @@ function formatPeriodCaps(caps) {
25438
25509
  var PRICE_UNITS = `a decimal dollar amount in the plan currency (major units, e.g. 9.99 for $9.99 \u2014 NOT cents, so 1999 means $1,999.00; a currency with no minor unit takes whole numbers, so 500 means \xA5500 and 500.5 is rejected; currency is one of ${SUPPORTED_CURRENCIES.join(", ")})`;
25439
25510
  var CREATE_PRODUCT_STORED_PLANS = "`plans` repeats what was stored for each plan, in `planIds` order: amount, currency, `price` formatted as the checkout shows it, and for recurring plans billingPeriod, periodLength and freeTrial \u2014 check `price` against what the user asked for. `warnings`, when present, flags an amount that looks like cents; relay it and, if the user meant the smaller price, fix it with update_plan before sharing the link.";
25440
25511
  var CREATE_ORDER_BOUNDS = `currency is one of ${SUPPORTED_CURRENCIES.join(", ")}. unitPrice is at most ${formatAmountCeilings()} in the order currency, and so is each line (unitPrice x quantity) and the order total; quantity is at most ${MAX_ORDER_ITEM_QUANTITY}; periodLength is at most one year of the billingPeriod (${formatPeriodCaps(MAX_ORDER_PERIOD_LENGTH_BY_BILLING_PERIOD)}). When billingPeriod is "day", periodLength must be at least ${MIN_WHOP_BILLING_PERIOD_DAYS} (the payment provider rejects true daily subscriptions). trialPeriodDays is at most ${MAX_FREE_TRIAL_DAYS}, only on recurring custom lines, and must match the trial length of every other recurring line in the order (catalogue plan or custom).`;
25512
+ var CREATE_ORDER_METADATA = ` Optional metadata: your own flat map of string keys to string values (for example { "cf_order_id": "A-1001" }), at most ${MAX_ORDER_METADATA_KEYS} keys, each key 1-${MAX_ORDER_METADATA_KEY_LENGTH} characters, each value at most ${MAX_ORDER_METADATA_VALUE_LENGTH} characters. It is stored on the order and returned in metadata exactly as sent (keys are not case-converted), including on the payment and subscription webhooks for the order; the buyer does not see it. Repeating an idempotencyKey returns the original order, including its original metadata; metadata on the repeat is ignored.`;
25441
25513
  var CREATE_PRODUCT_STATUS_NOTE = "Optional `status` ('draft' | 'active' | 'archived') defaults to 'active' \u2014 the product is created live and purchasable immediately via its checkout link, regardless of status. Pass `status: 'draft'` only when the end user explicitly asks for a draft or temporary/not-yet-ready product.";
25442
25514
  var CREATE_PRODUCT_COPY_CAPS = `Product \`name\` (<=${PRODUCT_NAME_MAX} chars), product \`description\` (<=${PRODUCT_DESCRIPTION_MAX} chars) and plan \`description\` (<=${PLAN_DESCRIPTION_MAX} chars) are all short one-line copy \u2014 if yours is longer, shorten it before calling and put the full version in that plan's \`richDescription\` (<=${PLAN_RICH_DESCRIPTION_MAX} chars), which the hosted checkout page renders as plain text beneath the short description (newlines preserved, markup not interpreted). \`richDescription\` is per plan, so product-level copy that overflows must still be edited down.`;
25443
25515
  var TRANSACTION_STATUS_RULE = 'Status: only "succeeded" means paid. "pending" is not yet confirmed by the payment provider, so never report a pending transaction as paid. "partially_refunded" means some but not all of the amount was refunded; "refunded", "failed" and "disputed" mean what they say.';
@@ -25446,14 +25518,14 @@ var TRANSACTION_SCOPE_RULE = 'Requires the "transaction:read" scope, which exist
25446
25518
  var LIST_TRANSACTIONS_SUMMARY = `List a merchant's transactions, newest first, narrowed by the optional filters in the input schema. For the full lookup guide, call search_payments_ai_docs with "transaction lookup".`;
25447
25519
  var GET_TRANSACTION_DETAIL = "Get one transaction by transactionId from list_transactions: the same fields plus the refunded amount and the order's line items. items is empty for a single-plan purchase, where plan names what was bought.";
25448
25520
  var TOOL_DESCRIPTIONS = {
25449
- [MCP_TOOL_NAMES.WHOAMI]: `Prove which account and which MCP server this connection reaches. Takes no arguments. Returns { userId, scopes, host, endpoint }, read from the validated token and the server that answered. "scopes" null means the token holds every scope except "transaction:read", which must be granted by name. "host" is the class of MCP URL this client is connected to: "live" for the main MCP URL, which serves every merchant at its own deployment, or "sandbox" for the sandbox-only MCP URL. It is not the environment (dev, stage or prod) and not a merchant's deployment: on a "live" host a merchant that has not gone live still reports deployment "sandbox" in get_my_merchant, which is expected. To prove the connection, quote userId, host and endpoint from this result rather than the client's connected label. Needs no scope: any valid token can call it. Call get_my_merchant next to find the merchants this connection owns.`,
25521
+ [MCP_TOOL_NAMES.WHOAMI]: `Prove which account and which MCP server this connection reaches. Takes no arguments. Returns { userId, scopes, host, endpoint, accountName, merchantIds, credentialExpiresAt, credentialExpiresInSeconds, credentialExpiryWarning? }, read from the validated token and the server that answered. "scopes" null means the token holds every scope except "transaction:read", which must be granted by name. "host" is the class of MCP URL this client is connected to: "live" for the main MCP URL, which serves every merchant at its own deployment, or "sandbox" for the sandbox-only MCP URL. It is not the environment (dev, stage or prod) and not a merchant's deployment: on a "live" host a merchant that has not gone live still reports deployment "sandbox" in get_my_merchant, which is expected. To prove the connection, quote userId, host and endpoint from this result rather than the client's connected label. "accountName" is the business name of the account you are connected to, never an email address: quote it with host (live or sandbox) and ask the developer to confirm it is their business; when it is null, the account has no business yet, so say so and quote userId. "merchantIds" are the merchants this credential is a member of; credentialExpiresAt and credentialExpiresInSeconds say when it stops working (null when unknown). credentialExpiryWarning is present only when the credential expires within ` + CREDENTIAL_EXPIRY_WARNING_WINDOW_DAYS + " days: tell the user and share its reapproveUrl, and expect the same warning appended to every other tool result until they re-approve. Needs no scope: any valid token can call it. Call get_my_merchant next to find the merchants this connection owns.",
25450
25522
  [MCP_TOOL_NAMES.GET_MY_MERCHANT]: 'List the merchant accounts this connection already owns. Call this BEFORE create_merchant_account: a new agent session cannot remember a merchant created in an earlier one, and creating again produces a second merchant with its own products and its own checkout links. Takes no arguments \u2014 the merchants come from the authorization on the connection itself. Returns { merchants: [{ merchantId, name, deployment }] }. "deployment" is "sandbox" before go_live and "live" after it has succeeded, and it matches where create_product and list_products will route for that merchant right now. An empty merchants array is a normal result, not an error: this connection owns no merchant yet, so call create_merchant_account. When more than one merchant comes back, ask which one to use \u2014 do not pick for the user. Requires the "merchant:read" scope.',
25451
25523
  [MCP_TOOL_NAMES.CREATE_MERCHANT_ACCOUNT]: "Create a merchant account on PAI Managed. Call get_my_merchant first: if it returns a merchant, reuse that merchantId instead of creating another, because a duplicate merchant gets its own products and checkout links. " + CREATE_MERCHANT_CONTACT_GUIDANCE + " The business name is a display label and is not globally unique \u2014 every downstream tool (create_product, list_products, checkout) keys on merchantId, so if two merchants happen to share a name, disambiguate them by merchantId (do not pick a distinct name here to work around it). No identity verification (KYC/KYB) is needed to use sandbox \u2014 products, checkout links and test transactions all work without it; verification is required on live. " + IDENTITY_VERIFICATION_RULE + " Dual-provisions live and sandbox with a shared merchantId. On partial dual-provision failure (one backend created it, the other did not), retry without a merchantId to finish it. When both backends fail, the returned error names the appropriate recovery \u2014 follow it as-is rather than repeating the same call in a loop, and treat repeated identical failures as needing escalation to support. Returns that merchant ID. Without a merchantId, a merchant this account already owns is returned with existing: true instead of a new one being created, and whichever of live or sandbox is missing it gets a copy with the details the existing copy holds; a merchantId this account already owns is finished the same way, and the copy that exists is never changed. Next: create_product (and checkout tools) on sandbox, then go_live when ready.",
25452
25524
  [MCP_TOOL_NAMES.GET_MERCHANT_ACTIVATION_STATUS]: 'Get the production activation status of a merchant account on PAI Managed. Returns { status, identityVerification }. status "pending": the merchant has not been promoted to live yet; this is the only status where go_live is the next step, once the user confirms the merchant is ready, and polling this tool will not change it on its own. status "verification_pending": the merchant is promoted to live and identity verification (KYC) is not complete yet; do not call go_live again. status "active": live and identity verified. status "not_provisioned": no live merchant record exists (dual provision never finished on live); recovery: re-run create_merchant_account without a merchantId, which finds this merchant and creates the missing live copy with the details the sandbox copy holds \u2014 go_live cannot fix this and fails while the live twin is missing. ' + IDENTITY_VERIFICATION_RULE + ' identityVerification is the KYC state: "not_started", "pending", "in_review", "verified" or "rejected".',
25453
25525
  [MCP_TOOL_NAMES.GO_LIVE]: 'IRREVERSIBLE. Promote a sandbox merchant to live by setting liveMerchantId (Merchant Go-Live). This is one-way: it assigns merchant-of-record liability and notifies KYC on the live backend, and there is no tool that undoes it. Confirm with the user before calling it. Validates deployment readiness (sandbox merchant exists, not linked to a different live id, live twin reachable), marks the sandbox merchant live with the dual-provision twin id, then best-effort notifies KYC on the live backend. Pass dryRun: true to run exactly those readiness checks and stop before anything is written \u2014 the response then carries dryRun: true, and its absence is your proof that a real promotion happened. Unknown arguments are rejected rather than ignored, so a guessed safety flag fails loudly instead of being silently dropped. Idempotent: a call for a merchant that is already live changes nothing and returns alreadyLive: true, so there is never a reason to call it twice. The response carries activationStatus, the same shape get_merchant_activation_status returns ("verification_pending" right after promotion until identity verification completes; omitted only when the live status cannot be read), and a real call also returns nextSteps to relay to the user. ' + LIVE_CATALOG_RULE + " " + IDENTITY_VERIFICATION_RULE + ' The identity-verification link goes by SMS to the merchant phone, or by email when there is no dialable phone; kycNotify.phoneIssue "invalid" means the phone number cannot be dialled, so tell the user it is invalid. Distinct from get_merchant_activation_status (activation and identity-verification status) and from ApiKeyEnvironment.',
25454
25526
  [MCP_TOOL_NAMES.SEARCH_PAYMENTS_AI_DOCS]: "Search the PAI Managed documentation knowledge base to find guides, API references, UI walkthroughs, and feature explanations. Returns matching sections with titles and direct documentation links. Use this tool to understand how PAI Managed works, find step-by-step instructions for dashboard tasks, or locate specific features such as creating a merchant account (phone / E.164), go-live, creating products, configuring plans, or generating checkout links.",
25455
25527
  [MCP_TOOL_NAMES.CREATE_PRODUCT]: "Create a product with one or more pricing plans for a merchant on the PAI Managed platform. Product and checkout mutations target sandbox until go_live. Products represent digital offerings (software, subscriptions, services) that customers can purchase via a checkout link. Each plan defines the price (" + PRICE_UNITS + "), billing type (one-time, recurring, or free-access), and billing period. " + CREATE_PRODUCT_PLAN_BOUNDS + " " + CREATE_PRODUCT_STATUS_NOTE + " " + CREATE_PRODUCT_COPY_CAPS + " Returns the product ID, the plan IDs, and ready-to-open hosted-checkout URLs: `checkoutUrl` for the first created plan and `planCheckoutUrls` (one `{planId, checkoutUrl}` entry per created plan) \u2014 do not construct them yourself. The URLs already reflect the merchant's go-live status, not your credentials: sandbox (`?isSandbox=true`) before go_live, live (no query param) once go_live has succeeded \u2014 nothing to switch on your side. " + CREATE_PRODUCT_STORED_PLANS + " Product images are not supported on create \u2014 after success, call get_product_image_upload_url with the returned id as productId, POST the file bytes (multipart/form-data) to the returned URL, then call confirm_product_image with the returned key.",
25456
- [MCP_TOOL_NAMES.CREATE_ORDER]: "Create a payable order for a merchant on PAI Managed. Use create_order when the buyer pays for two or more items, or when you set the price yourself. Use the checkoutUrl from create_product or list_products for a single catalogue plan. Provide merchantId, currency (3 letters, for example usd), and items. A catalogue item is { planId, quantity }. A caller-priced item is { label, unitPrice, quantity, type } where type is one_time or recurring; a recurring caller-priced item also needs billingPeriod (day, week, month, or year) and periodLength. unitPrice is decimal dollars, not cents (10 means $10.00). " + CREATE_ORDER_BOUNDS + ' Optional idempotencyKey: repeating the same key returns the same orderId; omitting it creates a new order. Returns orderId and checkoutUrl \u2014 use the URL verbatim, do not construct it yourself. The URL already reflects the merchant\'s go-live status: sandbox (`?isSandbox=true`) before go_live, live (no query param) once go_live has succeeded. Requires the "checkout:write" scope.',
25528
+ [MCP_TOOL_NAMES.CREATE_ORDER]: "Create a payable order for a merchant on PAI Managed. Use create_order when the buyer pays for two or more items, or when you set the price yourself. Use the checkoutUrl from create_product or list_products for a single catalogue plan. Provide merchantId, currency (3 letters, for example usd), and items. A catalogue item is { planId, quantity }. A caller-priced item is { label, unitPrice, quantity, type } where type is one_time or recurring; a recurring caller-priced item also needs billingPeriod (day, week, month, or year) and periodLength. unitPrice is decimal dollars, not cents (10 means $10.00). " + CREATE_ORDER_BOUNDS + CREATE_ORDER_METADATA + ' Optional idempotencyKey: repeating the same key returns the same orderId; omitting it creates a new order. Returns orderId and checkoutUrl \u2014 use the URL verbatim, do not construct it yourself. The URL already reflects the merchant\'s go-live status: sandbox (`?isSandbox=true`) before go_live, live (no query param) once go_live has succeeded. Requires the "checkout:write" scope.',
25457
25529
  [MCP_TOOL_NAMES.GET_PRODUCT_IMAGE_UPLOAD_URL]: 'Get a presigned POST policy to upload a product image directly to storage \u2014 the image bytes never pass through the tool call, and S3 enforces the 5 MB cap at upload time via `content-length-range` (oversized bodies get a 403 from S3, not the backend). Use this on any surface that can issue an HTTP multipart/form-data POST (CLI, agent, dashboard). The product must already exist (call create_product first). Provide merchantId, productId, fileName, and mimeType (image/png, image/jpeg, image/jpg, or image/webp). Returns { url, fields, key, expiresIn }: issue an HTTP POST to `url` with Content-Type: multipart/form-data. Add every entry from `fields` as a form field FIRST, then add the `file` field last (S3 requires the file field last), e.g. `curl -X POST <all -F fields...> -F "file=@./logo.png" "<url>"`. Then call confirm_product_image with the returned key to validate and attach the image.',
25458
25530
  [MCP_TOOL_NAMES.CONFIRM_PRODUCT_IMAGE]: "Confirm a product image uploaded via a presigned POST policy from get_product_image_upload_url. Call this after the multipart/form-data POST succeeds. Provide merchantId, productId, and the key returned by get_product_image_upload_url. The backend validates the stored object (format, size, magic bytes) and attaches it to the product. Returns { imageUrl } for verification. Replaces any existing product image.",
25459
25531
  [MCP_TOOL_NAMES.LIST_PRODUCTS]: "List products and pricing plans for a merchant on PAI Managed. Returns paginated products; each product carries a ready-to-open hosted-checkout `checkoutUrl`, and each plan summary (planId, name, amount, currency, `price` formatted as the checkout shows it, type, and for recurring plans billingPeriod, periodLength and freeTrial) carries its own `checkoutUrl` \u2014 use those directly, do not construct them yourself. The URLs already reflect the merchant's go-live status, not your credentials: sandbox (`?isSandbox=true`) before go_live, live (no query param) once go_live has succeeded \u2014 nothing to switch on your side. Optional filters: q (name search), status (draft|active|archived), type (one-time|recurring|free-access), dateRangeStart/dateRangeEnd, page, limit. Use before create_product when you need existing productId/planId values, or to share checkout links for plans that already exist. Targets sandbox until go_live, same as other product tools.",
@@ -25471,7 +25543,7 @@ var SOLO_TOOL_DESCRIPTION_OVERRIDES = {
25471
25543
  [MCP_TOOL_NAMES.GET_MERCHANT_ACTIVATION_STATUS]: 'Get the production activation status of a merchant account on this sandbox MCP URL. Live promotion is not available on this URL, so the status you will typically see is "pending". Returns { status }. status "pending": the merchant has not been promoted to live; connect to the live MCP URL to promote it \u2014 polling this tool on the sandbox URL will not change it. status "verification_pending": the merchant is promoted to live and identity verification (KYC) is not confirmed; follow verification on the live MCP URL, which also reports the identity-verification state. status "active": live and identity verified; connect to the live MCP URL for live operations. status "not_provisioned": no merchant record exists on sandbox; recovery: connect to the live MCP URL and re-run create_merchant_account there without a merchantId, which creates the missing sandbox copy with the details the live copy holds.',
25472
25544
  [MCP_TOOL_NAMES.SEARCH_PAYMENTS_AI_DOCS]: "Search the PAI Managed documentation knowledge base to find guides, API references, UI walkthroughs, and feature explanations. Returns matching sections with titles and direct documentation links. Use this tool to understand how PAI Managed works, find step-by-step instructions for dashboard tasks, or locate specific features such as creating a merchant account (phone / E.164), creating products, configuring plans, or generating checkout links. This sandbox MCP URL cannot call go_live; product and checkout writes stay in sandbox.",
25473
25545
  [MCP_TOOL_NAMES.CREATE_PRODUCT]: "Create a product with one or more pricing plans for a merchant on Payments AI sandbox. Product and checkout mutations stay on sandbox on this MCP URL. Products represent digital offerings (software, subscriptions, services) that customers can purchase via a checkout link. Each plan defines the price (" + PRICE_UNITS + "), billing type (one-time, recurring, or free-access), and billing period. " + CREATE_PRODUCT_PLAN_BOUNDS + " " + CREATE_PRODUCT_STATUS_NOTE + " " + CREATE_PRODUCT_COPY_CAPS + " Returns the product ID, the plan IDs, and ready-to-open hosted-checkout URLs: `checkoutUrl` for the first created plan and `planCheckoutUrls` (one `{planId, checkoutUrl}` entry per created plan) \u2014 do not construct them yourself. The URLs are sandbox checkout links (`?isSandbox=true`). " + CREATE_PRODUCT_STORED_PLANS + " " + SANDBOX_TEST_CARD_NOTE + " Product images are not supported on create \u2014 after success, call get_product_image_upload_url with the returned id as productId, POST the file bytes (multipart/form-data) to the returned URL, then call confirm_product_image with the returned key.",
25474
- [MCP_TOOL_NAMES.CREATE_ORDER]: "Create a payable order for a merchant on Payments AI sandbox. Use create_order when the buyer pays for two or more items, or when you set the price yourself. Use the checkoutUrl from create_product or list_products for a single catalogue plan. Provide merchantId, currency (3 letters, for example usd), and items. A catalogue item is { planId, quantity }. A caller-priced item is { label, unitPrice, quantity, type } where type is one_time or recurring; a recurring caller-priced item also needs billingPeriod (day, week, month, or year) and periodLength. unitPrice is decimal dollars, not cents (10 means $10.00). " + CREATE_ORDER_BOUNDS + ' Optional idempotencyKey: repeating the same key returns the same orderId; omitting it creates a new order. Returns orderId and checkoutUrl \u2014 use the URL verbatim, do not construct it yourself. The URL is a sandbox checkout link (`?isSandbox=true`). Requires the "checkout:write" scope. Stays on sandbox on this MCP URL. ' + SANDBOX_TEST_CARD_NOTE,
25546
+ [MCP_TOOL_NAMES.CREATE_ORDER]: "Create a payable order for a merchant on Payments AI sandbox. Use create_order when the buyer pays for two or more items, or when you set the price yourself. Use the checkoutUrl from create_product or list_products for a single catalogue plan. Provide merchantId, currency (3 letters, for example usd), and items. A catalogue item is { planId, quantity }. A caller-priced item is { label, unitPrice, quantity, type } where type is one_time or recurring; a recurring caller-priced item also needs billingPeriod (day, week, month, or year) and periodLength. unitPrice is decimal dollars, not cents (10 means $10.00). " + CREATE_ORDER_BOUNDS + CREATE_ORDER_METADATA + ' Optional idempotencyKey: repeating the same key returns the same orderId; omitting it creates a new order. Returns orderId and checkoutUrl \u2014 use the URL verbatim, do not construct it yourself. The URL is a sandbox checkout link (`?isSandbox=true`). Requires the "checkout:write" scope. Stays on sandbox on this MCP URL. ' + SANDBOX_TEST_CARD_NOTE,
25475
25547
  [MCP_TOOL_NAMES.LIST_PRODUCTS]: "List products and pricing plans for a merchant on Payments AI sandbox. Returns paginated products; each product carries a ready-to-open hosted-checkout `checkoutUrl`, and each plan summary (planId, name, amount, currency, `price` formatted as the checkout shows it, type, and for recurring plans billingPeriod, periodLength and freeTrial) carries its own `checkoutUrl` \u2014 use those directly, do not construct them yourself. The URLs are sandbox checkout links (`?isSandbox=true`). Optional filters: q (name search), status (draft|active|archived), type (one-time|recurring|free-access), dateRangeStart/dateRangeEnd, page, limit. Use before create_product when you need existing productId/planId values, or to share checkout links for plans that already exist. Stays on sandbox on this MCP URL, same as other product tools.",
25476
25548
  [MCP_TOOL_NAMES.LIST_TRANSACTIONS]: LIST_TRANSACTIONS_SUMMARY + ' This sandbox MCP URL returns sandbox transactions only; deployment "live" is refused. The top-level deployment names where it searched; an empty data list means no transaction matches there, not an error. ' + TRANSACTION_STATUS_RULE + " " + TRANSACTION_REFUND_RULE + " " + TRANSACTION_SCOPE_RULE,
25477
25549
  [MCP_TOOL_NAMES.GET_TRANSACTION]: GET_TRANSACTION_DETAIL + ' This sandbox MCP URL returns sandbox transactions only; deployment "live" is refused. ' + TRANSACTION_STATUS_RULE + " " + TRANSACTION_REFUND_RULE + " " + TRANSACTION_SCOPE_RULE
@@ -25558,11 +25630,13 @@ var QUICKSTART_SKILL_SECTIONS = [
25558
25630
  "",
25559
25631
  'Do not trust the client\'s "Connected" label. Call `whoami` and report what it returns. `whoami` takes no arguments and needs no scope: every valid token can call it.',
25560
25632
  "",
25561
- "| Result | Meaning | Tell the developer |",
25562
- "| -------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |",
25563
- "| Success | Connected. | Quote `userId`, `host` (`sandbox` or `live`: the MCP URL connected to, not the merchant deployment) and `endpoint`. `scopes: null` means every scope except `transaction:read`. |",
25564
- "| HTTP 401 | Not connected. | Quote the 401 `message` verbatim (for `code: token_expired` it says where to generate a new token), then show the MCP config above. |",
25565
- "| Any tool error | Connected, but the server could not prove the connection. | Quote the error. Do not say disconnected and do not paste the connect config. |",
25633
+ "| Result | Meaning | Tell the developer |",
25634
+ "| -------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |",
25635
+ "| Success | Connected. | Quote `accountName` (the business name of the account you are connected to, never an email) and ask the developer to confirm it, together with `host` (live or sandbox), is their own business account. Then quote `userId`, `host` (`sandbox` or `live`: the MCP URL connected to, not the merchant deployment) and `endpoint`. `accountName: null` means no label is known: say so. `scopes: null` means every scope except `transaction:read`. |",
25636
+ "| HTTP 401 | Not connected. | Quote the 401 `message` verbatim (for `code: token_expired` it says how to reconnect: re-approve in the client for OAuth, run the device grant again and replace the `Authorization` header if connected that way, or generate a new token if connected with a token), then show the MCP config above. |",
25637
+ "| Any tool error | Connected, but the server could not prove the connection. | Quote the error. Do not say disconnected and do not paste the connect config. |",
25638
+ "",
25639
+ 'If `whoami` returns a `credentialExpiryWarning`, or a tool result carries one under `_meta["ai.payments/credentialExpiryWarning"]` with a closing text block (OAuth connections only, within 14 days of expiry), tell the user and share its re-approve link, which opens the connect page. If you connected with the device grant, run it again and replace the `Authorization` header instead (see "Connect without a terminal"). `credentialExpiresAt` and `credentialExpiresInSeconds` say when the credential stops working; `null` means unknown.',
25566
25640
  "",
25567
25641
  "## Before you mutate anything",
25568
25642
  "",
@@ -25583,7 +25657,7 @@ var QUICKSTART_SKILL_SECTIONS = [
25583
25657
  "`merchants` list means connected with no merchant yet. If the call is denied for",
25584
25658
  "`merchant:read`, name that missing scope; do not say disconnected.",
25585
25659
  "",
25586
- "Ask for business **name**, **email**, and **phone** in E.164 (`+14155552671`). Call `create_merchant_account`. Show `merchantId`.",
25660
+ "Ask for business **name** and **email**. **Phone** is optional: include it (the user's own mobile, E.164, e.g. `+14155552671`) only if they offer one; never invent a placeholder, and omit it otherwise. Call `create_merchant_account`. Show `merchantId`.",
25587
25661
  "",
25588
25662
  "### A2. Product",
25589
25663
  "",
@@ -25597,15 +25671,9 @@ var QUICKSTART_SKILL_SECTIONS = [
25597
25671
  "",
25598
25672
  "### A3. Checkout URL",
25599
25673
  "",
25600
- "Sandbox testing:",
25601
- "",
25602
- "`{{MANAGED_API_HOST_URL}}/payment/{planId}?isSandbox=true`",
25674
+ "Use the link the tool returns verbatim: `planCheckoutUrls[i].checkoutUrl` from `create_product` or `plans[i].checkoutUrl` from `list_products`. The top-level `checkoutUrl` is the first plan's link only. Never build a checkout URL from a `planId`.",
25603
25675
  "",
25604
- "Live (only after `go_live` **and** re-running `create_product` there \u2014 `go_live` does not create the plan on live, so this URL 404s until you do):",
25605
- "",
25606
- "`{{MANAGED_API_HOST_URL}}/payment/{planId}`",
25607
- "",
25608
- "`list_products` also returns `checkoutUrl` per plan. A sandbox checkout link never becomes this URL on its own; it keeps `?isSandbox=true` forever.",
25676
+ "Sandbox and live are separate backends and `go_live` does not copy products or plans: sandbox plans do not exist on live. To get a live link, call `create_product` again after `go_live` and use the `checkoutUrl` it returns. A sandbox checkout link keeps `?isSandbox=true` forever and never becomes a live link.",
25609
25677
  "",
25610
25678
  "Test card (Sandbox): `4242 4242 4242 4242`, any future expiry, any CVC. Refused-payment card (Sandbox): `4000 0000 0000 0002`. Give both to the user with every sandbox checkout link. Test cards never work on Live: live checkout links take real cards only.",
25611
25679
  "",
@@ -25630,24 +25698,26 @@ var QUICKSTART_SKILL_SECTIONS = [
25630
25698
  "",
25631
25699
  "## Tools",
25632
25700
  "",
25633
- "| Tool | Use |",
25634
- "| -------------------------------- | ------------------------------------------------------- |",
25635
- "| `whoami` | `userId`, `scopes`, `host`, `endpoint`; any valid token |",
25636
- "| `search_payments_ai_docs` | Search developer docs |",
25637
- "| `get_my_merchant` | Owned merchants |",
25638
- "| `create_merchant_account` | Dual-provision live + sandbox merchant |",
25639
- "| `get_merchant_activation_status` | Activation status (see below) |",
25640
- "| `go_live` | Promote to live; a repeat returns current status |",
25641
- "| `create_product` | Product + plans (sandbox until go-live) |",
25642
- "| `create_order` | Multi-item or caller-priced checkout |",
25643
- "| `list_products` | List products, plans, checkout URLs |",
25644
- "| `list_transactions` | Look up transactions (needs `transaction:read`) |",
25645
- "| `get_transaction` | One transaction's details and line items |",
25646
- "| `get_product_image_upload_url` | Presigned POST for product image |",
25647
- "| `confirm_product_image` | Attach uploaded product image |",
25648
- "| `get_checkout_customization` | Read hosted checkout branding |",
25649
- "| `update_checkout_customization` | Merge-update branding |",
25650
- "| `set_checkout_logo` | Mint \u2192 PUT \u2192 confirm checkout logo |",
25701
+ "| Tool | Use |",
25702
+ "| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |",
25703
+ "| `whoami` | `accountName`, `userId`, `scopes`, `host`, `endpoint`, `merchantIds`, `credentialExpiresAt`, `credentialExpiresInSeconds`, `credentialExpiryWarning`; any valid token |",
25704
+ "| `search_payments_ai_docs` | Search developer docs |",
25705
+ "| `get_my_merchant` | Owned merchants |",
25706
+ "| `create_merchant_account` | Dual-provision live + sandbox merchant |",
25707
+ "| `get_merchant_activation_status` | Activation status (see below) |",
25708
+ "| `go_live` | Promote to live; a repeat returns current status |",
25709
+ "| `create_product` | Product + plans (sandbox until go-live) |",
25710
+ "| `create_order` | Multi-item or caller-priced checkout |",
25711
+ "| `update_product` | Merge-update a product's name, description or status |",
25712
+ "| `update_plan` | Merge-update a plan's name, description, price, billing period or free trial |",
25713
+ "| `list_products` | List products, plans, checkout URLs |",
25714
+ "| `list_transactions` | Look up transactions (needs `transaction:read`) |",
25715
+ "| `get_transaction` | One transaction's details and line items |",
25716
+ "| `get_product_image_upload_url` | Presigned POST for product image |",
25717
+ "| `confirm_product_image` | Attach uploaded product image |",
25718
+ "| `get_checkout_customization` | Read hosted checkout branding |",
25719
+ "| `update_checkout_customization` | Merge-update branding |",
25720
+ "| `set_checkout_logo` | Mint \u2192 PUT \u2192 confirm checkout logo |",
25651
25721
  "",
25652
25722
  "### Activation status",
25653
25723
  "",
@@ -25715,6 +25785,115 @@ async function resolveTransactionLookupBackend(input2) {
25715
25785
  return { error: NOT_LIVE_YET_ERROR };
25716
25786
  }
25717
25787
 
25788
+ // src/auth/mcp-auth-rejection.ts
25789
+ var MCP_AUTH_ERROR_DOC_SLUG = {
25790
+ missing_token: "missing-token",
25791
+ invalid_token: "invalid-token",
25792
+ token_expired: "token-expired"
25793
+ };
25794
+ var DEPLOYMENT_LABEL = {
25795
+ sandbox: "Sandbox",
25796
+ live: "Live"
25797
+ };
25798
+ var MCP_TOKENS_DASHBOARD_PATH = "Developer Tools \u2192 MCP Tokens";
25799
+ var MESSAGE_BY_CODE = {
25800
+ missing_token: () => `No MCP credential was sent. Send an "Authorization: Bearer <token>" header with a pai_mcp_ token from ${MCP_TOKENS_DASHBOARD_PATH}, or connect with OAuth.`,
25801
+ invalid_token: (context) => {
25802
+ const here = DEPLOYMENT_LABEL[context.serverDeployment];
25803
+ return `This MCP token is not valid on this ${here} MCP URL: it is unknown, revoked or malformed. Tokens generated on the ${here} tab of ${MCP_TOKENS_DASHBOARD_PATH} work only with the ${here} MCP URL. Generate a new token there.`;
25804
+ },
25805
+ token_expired: (context) => `${context.expiredMessage ?? MCP_SESSION_EXPIRED_MESSAGE} How to reconnect: ${context.connectPageUrl}`,
25806
+ token_validation_unavailable: () => "The MCP token could not be validated because the backend is unavailable. Retry in a moment; do not generate a new token."
25807
+ };
25808
+ function resolveDocsBase(docsBaseUrl2) {
25809
+ return (docsBaseUrl2 ?? DEFAULT_DOCS_BASE_URL).replace(/\/+$/, "");
25810
+ }
25811
+ function buildMcpAuthErrorDocUrl(code, docsBaseUrl2) {
25812
+ const base = resolveDocsBase(docsBaseUrl2);
25813
+ const slug = MCP_AUTH_ERROR_DOC_SLUG[code];
25814
+ return slug === void 0 ? `${base}/errors` : `${base}/errors/${slug}`;
25815
+ }
25816
+ function buildMcpConnectPageUrl(docsBaseUrl2) {
25817
+ return `${resolveDocsBase(docsBaseUrl2)}/connect`;
25818
+ }
25819
+ function buildMcpConnectDiagnosticsUrl(mcpPath2, mcpPublicUrl2) {
25820
+ const endpoint = mcpPublicUrl2 === void 0 ? mcpPath2 : mcpPublicUrl2.replace(/\/+$/, "");
25821
+ return `${endpoint}${CONNECT_DIAGNOSTICS_PATH_SUFFIX}`;
25822
+ }
25823
+ function buildMcpAuthRejectionBody(code, context, links) {
25824
+ const connectPageUrl = buildMcpConnectPageUrl(links.docsBaseUrl);
25825
+ return {
25826
+ statusCode: 401,
25827
+ error: "Unauthorized",
25828
+ code,
25829
+ message: MESSAGE_BY_CODE[code]({ ...context, connectPageUrl }),
25830
+ docUrl: buildMcpAuthErrorDocUrl(code, links.docsBaseUrl),
25831
+ connectDiagnosticsUrl: links.connectDiagnosticsUrl,
25832
+ connectPageUrl
25833
+ };
25834
+ }
25835
+ function formatMcpAuthRejectionLog(rejection, mode) {
25836
+ return JSON.stringify({
25837
+ event: "mcp.auth_rejected",
25838
+ code: rejection.code,
25839
+ reason: rejection.reason,
25840
+ mode
25841
+ });
25842
+ }
25843
+
25844
+ // src/auth/credential-expiry.ts
25845
+ var SECONDS_PER_HOUR = 60 * 60;
25846
+ var SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
25847
+ var NO_EXPIRY = {
25848
+ credentialExpiresAt: null,
25849
+ credentialExpiresInSeconds: null,
25850
+ warning: void 0
25851
+ };
25852
+ function plural2(count, unit) {
25853
+ return `${count} ${unit}${count === 1 ? "" : "s"}`;
25854
+ }
25855
+ function describeRemaining(seconds) {
25856
+ if (seconds >= SECONDS_PER_DAY) {
25857
+ return plural2(Math.floor(seconds / SECONDS_PER_DAY), "day");
25858
+ }
25859
+ if (seconds >= SECONDS_PER_HOUR) {
25860
+ return plural2(Math.floor(seconds / SECONDS_PER_HOUR), "hour");
25861
+ }
25862
+ return "less than an hour";
25863
+ }
25864
+ function formatWarningMessage(expiresAt, seconds, reapproveUrl) {
25865
+ if (seconds === 0) {
25866
+ return `This MCP credential has expired (${expiresAt}). Tell the user to re-approve this connection so it works again: ${reapproveUrl}`;
25867
+ }
25868
+ return `This MCP credential expires in ${describeRemaining(seconds)} (${expiresAt}). Tell the user to re-approve this connection before then so it keeps working: ${reapproveUrl}`;
25869
+ }
25870
+ function resolveCredentialExpiry(input2) {
25871
+ const { expiresAt, now, docsBaseUrl: docsBaseUrl2, warnsBeforeExpiry } = input2;
25872
+ if (expiresAt === void 0) {
25873
+ return NO_EXPIRY;
25874
+ }
25875
+ const remainingMs = Date.parse(expiresAt) - now.getTime();
25876
+ const seconds = Math.max(0, Math.floor(remainingMs / 1e3));
25877
+ if (!warnsBeforeExpiry || seconds > CREDENTIAL_EXPIRY_WARNING_WINDOW_SECONDS) {
25878
+ return {
25879
+ credentialExpiresAt: expiresAt,
25880
+ credentialExpiresInSeconds: seconds,
25881
+ warning: void 0
25882
+ };
25883
+ }
25884
+ const reapproveUrl = buildMcpConnectPageUrl(docsBaseUrl2);
25885
+ return {
25886
+ credentialExpiresAt: expiresAt,
25887
+ credentialExpiresInSeconds: seconds,
25888
+ warning: {
25889
+ credentialExpiresAt: expiresAt,
25890
+ credentialExpiresInSeconds: seconds,
25891
+ reapproveUrl,
25892
+ message: formatWarningMessage(expiresAt, seconds, reapproveUrl)
25893
+ }
25894
+ };
25895
+ }
25896
+
25718
25897
  // src/tools/tool-required-scopes.ts
25719
25898
  var ANY_VALID_TOKEN = /* @__PURE__ */ Symbol("ANY_VALID_TOKEN");
25720
25899
  var TOOL_REQUIRED_SCOPE = {
@@ -25787,6 +25966,36 @@ function filterToolsByScope(server, tokenPayload) {
25787
25966
  });
25788
25967
  }
25789
25968
 
25969
+ // src/tools/credential-expiry-warning.ts
25970
+ function hasContentBlocks(result2) {
25971
+ return typeof result2 === "object" && result2 !== null && Array.isArray(result2.content);
25972
+ }
25973
+ function withCredentialExpiryWarning(result2, warning) {
25974
+ if (!hasContentBlocks(result2)) {
25975
+ return result2;
25976
+ }
25977
+ return {
25978
+ ...result2,
25979
+ content: [...result2.content, { type: "text", text: warning.message }],
25980
+ // eslint-disable-next-line no-underscore-dangle -- MCP names the result metadata field `_meta`
25981
+ _meta: { ...result2._meta, [CREDENTIAL_EXPIRY_WARNING_META_KEY]: warning }
25982
+ };
25983
+ }
25984
+ function attachCredentialExpiryWarning(server, warning) {
25985
+ if (!warning) {
25986
+ return;
25987
+ }
25988
+ const protocol = server.server;
25989
+ const callTool = protocol._getRequestHandler?.("tools/call");
25990
+ if (!callTool) {
25991
+ throw new Error("The SDK tools/call handler is not available; register tools before wrapping");
25992
+ }
25993
+ server.server.setRequestHandler(
25994
+ "tools/call",
25995
+ async (request, ctx) => withCredentialExpiryWarning(await callTool(request, ctx), warning)
25996
+ );
25997
+ }
25998
+
25790
25999
  // src/tools/product/build-plan-checkout-url.ts
25791
26000
  var CHECKOUT_SANDBOX_QUERY_PARAM = "isSandbox";
25792
26001
  function buildPlanCheckoutUrl(params) {
@@ -26479,6 +26688,8 @@ var ORDER_DOC_SECTIONS = [
26479
26688
 
26480
26689
  **create_order** takes \`merchantId\`, \`currency\` (3 letters, for example usd), and \`items\`. A catalogue item is \`{ planId, quantity }\`. A caller-priced item is \`{ label, unitPrice, quantity, type }\` where \`type\` is \`one_time\` or \`recurring\`. A recurring caller-priced item also needs \`billingPeriod\` (\`day\`, \`week\`, \`month\`, or \`year\`) and \`periodLength\`. \`unitPrice\` is decimal dollars, not cents. Optional \`idempotencyKey\`: the same key returns the same \`orderId\`; omitting it creates a new order.
26481
26690
 
26691
+ Optional \`metadata\` stores your own references on the order, for example \`{ "cf_order_id": "A-1001" }\`: a flat map of string keys to string values, at most ${MAX_ORDER_METADATA_KEYS} keys, each key 1-${MAX_ORDER_METADATA_KEY_LENGTH} characters, each value at most ${MAX_ORDER_METADATA_VALUE_LENGTH} characters. Keys and values are kept and returned in \`metadata\` exactly as sent (\`cf_order_id\`, \`cfOrderId\` and \`Order-Ref\` stay distinct), and payment and subscription webhooks for the order carry the same \`metadata\`. The buyer does not see them. Repeating an idempotencyKey returns the original order, including its original metadata; metadata on the repeat is ignored. The Public API create-order endpoint takes the same \`metadata\`.
26692
+
26482
26693
  The tool returns \`orderId\` and \`checkoutUrl\`. Use \`checkoutUrl\` verbatim \u2014 do not build one yourself. Sandbox URLs carry \`?isSandbox=true\`. The merchant's go-live status selects sandbox versus live. Requires the \`checkout:write\` scope.`
26483
26694
  }
26484
26695
  ];
@@ -26629,7 +26840,7 @@ function runSearchPaymentsAiDocsHandler(query) {
26629
26840
  // src/tools/connection/connection-proof.ts
26630
26841
  var CONNECTION_PROOF_UNAVAILABLE_ERROR = "Could not prove this MCP connection: the server has no validated token or public MCP URL for this request. Contact Payments AI support.";
26631
26842
  function buildConnectionProof(input2) {
26632
- const { payload, host: host2, endpoint } = input2;
26843
+ const { payload, host: host2, endpoint, credentialExpiry } = input2;
26633
26844
  if (!payload || !host2 || !endpoint) {
26634
26845
  return void 0;
26635
26846
  }
@@ -26637,7 +26848,12 @@ function buildConnectionProof(input2) {
26637
26848
  userId: payload.userId,
26638
26849
  scopes: payload.scopes,
26639
26850
  host: host2,
26640
- endpoint
26851
+ endpoint,
26852
+ accountName: payload.accountName ?? null,
26853
+ merchantIds: payload.merchantIds,
26854
+ credentialExpiresAt: credentialExpiry.credentialExpiresAt,
26855
+ credentialExpiresInSeconds: credentialExpiry.credentialExpiresInSeconds,
26856
+ ...credentialExpiry.warning ? { credentialExpiryWarning: credentialExpiry.warning } : {}
26641
26857
  });
26642
26858
  return parsed.success ? parsed.data : void 0;
26643
26859
  }
@@ -27002,7 +27218,7 @@ function toolError(text) {
27002
27218
  };
27003
27219
  }
27004
27220
  function resolveBackendClients(options2) {
27005
- if (options2?.backendClients) {
27221
+ if (options2.backendClients) {
27006
27222
  return {
27007
27223
  sandbox: options2.backendClients.sandbox,
27008
27224
  live: options2.backendClients.live
@@ -27025,12 +27241,18 @@ async function resolveProductCheckoutRoute(sandbox, live, merchantId) {
27025
27241
  function registerTools(server, options2) {
27026
27242
  const { sandbox: sandboxBackendClient, live: liveBackendClient } = resolveBackendClients(options2);
27027
27243
  const dualBackendClients = sandboxBackendClient ? { sandbox: sandboxBackendClient, live: liveBackendClient } : void 0;
27028
- const deploymentRouter2 = options2?.deploymentRouter ?? (dualBackendClients && hasLiveBackend(dualBackendClients) ? createDualRouter(dualBackendClients) : void 0);
27029
- const serverDeployment = options2?.serverDeployment ?? deploymentRouter2?.serverDeployment;
27030
- const mcpTokenPayload = options2?.mcpTokenPayload;
27031
- const checkoutBaseUrl2 = options2?.checkoutBaseUrl ?? resolveCheckoutBaseUrl();
27032
- const dashboardUrl = options2?.dashboardUrl;
27244
+ const deploymentRouter2 = options2.deploymentRouter ?? (dualBackendClients && hasLiveBackend(dualBackendClients) ? createDualRouter(dualBackendClients) : void 0);
27245
+ const serverDeployment = options2.serverDeployment ?? deploymentRouter2?.serverDeployment;
27246
+ const mcpTokenPayload = options2.mcpTokenPayload;
27247
+ const checkoutBaseUrl2 = options2.checkoutBaseUrl ?? resolveCheckoutBaseUrl();
27248
+ const dashboardUrl = options2.dashboardUrl;
27033
27249
  const toolDescriptions = resolveToolDescriptions(deploymentRouter2?.mode ?? "dual");
27250
+ const credentialExpiry = resolveCredentialExpiry({
27251
+ expiresAt: mcpTokenPayload?.expiresAt,
27252
+ now: options2.now(),
27253
+ docsBaseUrl: options2.docsBaseUrl,
27254
+ warnsBeforeExpiry: mcpTokenPayload?.oauthIssued === true
27255
+ });
27034
27256
  const denyForTool = (name) => scopeDenial(TOOL_REQUIRED_SCOPE[name], mcpTokenPayload, dashboardUrl);
27035
27257
  async function resolveCallerMerchantIds(router, payload) {
27036
27258
  const allowlist = await router.resolveMerchantAllowlist(payload);
@@ -27093,7 +27315,8 @@ function registerTools(server, options2) {
27093
27315
  return runWhoamiHandler({
27094
27316
  payload: mcpTokenPayload,
27095
27317
  host: serverDeployment,
27096
- endpoint: options2?.mcpPublicUrl
27318
+ endpoint: options2.mcpPublicUrl,
27319
+ credentialExpiry
27097
27320
  });
27098
27321
  }
27099
27322
  );
@@ -27703,6 +27926,7 @@ function registerTools(server, options2) {
27703
27926
  }
27704
27927
  );
27705
27928
  filterToolsByScope(server, mcpTokenPayload);
27929
+ attachCredentialExpiryWarning(server, credentialExpiry.warning);
27706
27930
  }
27707
27931
 
27708
27932
  // src/http/connect-diagnostics/connect-diagnostics-intake.ts
@@ -28042,17 +28266,29 @@ function parseMcpTokenPayload(parsed) {
28042
28266
  if (typeof parsed !== "object" || parsed === null) {
28043
28267
  return void 0;
28044
28268
  }
28045
- const { userId, cognitoSub, merchantIds, scopes } = parsed;
28269
+ const { userId, cognitoSub, merchantIds, scopes, expiresAt, oauthIssued, accountName } = parsed;
28046
28270
  if (typeof userId !== "string" || typeof cognitoSub !== "string" || !isStringArray(merchantIds)) {
28047
28271
  return void 0;
28048
28272
  }
28273
+ const expiry = parseExpiresAt(expiresAt);
28274
+ const label = mcpAccountNameSchema.safeParse(accountName);
28049
28275
  return {
28050
28276
  userId,
28051
28277
  cognitoSub,
28052
28278
  merchantIds,
28053
- scopes: Array.isArray(scopes) ? scopes : null
28279
+ scopes: Array.isArray(scopes) ? scopes : null,
28280
+ oauthIssued: oauthIssued === true,
28281
+ ...expiry === void 0 ? {} : { expiresAt: expiry },
28282
+ ...label.success ? { accountName: label.data } : {}
28054
28283
  };
28055
28284
  }
28285
+ function parseExpiresAt(value) {
28286
+ if (typeof value !== "string") {
28287
+ return void 0;
28288
+ }
28289
+ const epochMs = Date.parse(value);
28290
+ return Number.isNaN(epochMs) ? void 0 : new Date(epochMs).toISOString();
28291
+ }
28056
28292
 
28057
28293
  // src/auth/mcp-token-audience.ts
28058
28294
  function isTokenAudienceAccepted(audience, mcpPublicUrl2) {
@@ -28066,63 +28302,17 @@ function isTokenAudienceAccepted(audience, mcpPublicUrl2) {
28066
28302
  return canonicalAudience !== null && canonicalAudience === canonicalResourceUrl(mcpPublicUrl2);
28067
28303
  }
28068
28304
 
28069
- // src/auth/mcp-auth-rejection.ts
28070
- var MCP_AUTH_ERROR_DOC_SLUG = {
28071
- missing_token: "missing-token",
28072
- invalid_token: "invalid-token",
28073
- token_expired: "token-expired"
28074
- };
28075
- var DEPLOYMENT_LABEL = {
28076
- sandbox: "Sandbox",
28077
- live: "Live"
28078
- };
28079
- var MCP_TOKENS_DASHBOARD_PATH = "Developer Tools \u2192 MCP Tokens";
28080
- var MESSAGE_BY_CODE = {
28081
- missing_token: () => `No MCP credential was sent. Send an "Authorization: Bearer <token>" header with a pai_mcp_ token from ${MCP_TOKENS_DASHBOARD_PATH}, or connect with OAuth.`,
28082
- invalid_token: (context) => {
28083
- const here = DEPLOYMENT_LABEL[context.serverDeployment];
28084
- return `This MCP token is not valid on this ${here} MCP URL: it is unknown, revoked or malformed. Tokens generated on the ${here} tab of ${MCP_TOKENS_DASHBOARD_PATH} work only with the ${here} MCP URL. Generate a new token there.`;
28085
- },
28086
- token_expired: (context) => context.expiredMessage ?? MCP_SESSION_EXPIRED_MESSAGE,
28087
- token_validation_unavailable: () => "The MCP token could not be validated because the backend is unavailable. Retry in a moment; do not generate a new token."
28088
- };
28089
- function resolveDocsBase(docsBaseUrl2) {
28090
- return (docsBaseUrl2 ?? DEFAULT_DOCS_BASE_URL).replace(/\/+$/, "");
28091
- }
28092
- function buildMcpAuthErrorDocUrl(code, docsBaseUrl2) {
28093
- const base = resolveDocsBase(docsBaseUrl2);
28094
- const slug = MCP_AUTH_ERROR_DOC_SLUG[code];
28095
- return slug === void 0 ? `${base}/errors` : `${base}/errors/${slug}`;
28096
- }
28097
- function buildMcpConnectPageUrl(docsBaseUrl2) {
28098
- return `${resolveDocsBase(docsBaseUrl2)}/connect`;
28099
- }
28100
- function buildMcpConnectDiagnosticsUrl(mcpPath2, mcpPublicUrl2) {
28101
- const endpoint = mcpPublicUrl2 === void 0 ? mcpPath2 : mcpPublicUrl2.replace(/\/+$/, "");
28102
- return `${endpoint}${CONNECT_DIAGNOSTICS_PATH_SUFFIX}`;
28103
- }
28104
- function buildMcpAuthRejectionBody(code, context, links) {
28105
- return {
28106
- statusCode: 401,
28107
- error: "Unauthorized",
28108
- code,
28109
- message: MESSAGE_BY_CODE[code](context),
28110
- docUrl: buildMcpAuthErrorDocUrl(code, links.docsBaseUrl),
28111
- connectDiagnosticsUrl: links.connectDiagnosticsUrl,
28112
- connectPageUrl: buildMcpConnectPageUrl(links.docsBaseUrl)
28113
- };
28114
- }
28115
- function formatMcpAuthRejectionLog(code, mode) {
28116
- return JSON.stringify({ event: "mcp.auth_rejected", code, mode });
28117
- }
28118
-
28119
28305
  // src/http/app.ts
28120
28306
  var LOCALHOST_BINDS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
28121
28307
  var LOCALHOST_ALLOWED_HOSTS = ["127.0.0.1", "localhost", "::1", "[::1]"];
28122
28308
  var BEARER_PREFIX = "Bearer ";
28123
28309
  var MCP_TOKEN_FORMAT = /^pai_mcp_[0-9a-f]{64}$/;
28124
- function reject(code, expiredMessage) {
28125
- return { ok: false, code, expiredMessage };
28310
+ function reject(code, options2 = {}) {
28311
+ return {
28312
+ ok: false,
28313
+ rejection: { code, reason: options2.reason ?? code },
28314
+ expiredMessage: options2.expiredMessage
28315
+ };
28126
28316
  }
28127
28317
  function decideFromOutcome(token, outcome, mcpPublicUrl2) {
28128
28318
  if (outcome.ok) {
@@ -28130,7 +28320,7 @@ function decideFromOutcome(token, outcome, mcpPublicUrl2) {
28130
28320
  }
28131
28321
  switch (outcome.reason) {
28132
28322
  case "expired":
28133
- return reject("token_expired", outcome.expiredMessage);
28323
+ return reject("token_expired", { expiredMessage: outcome.expiredMessage });
28134
28324
  case "unavailable":
28135
28325
  return reject("token_validation_unavailable");
28136
28326
  case "not_found":
@@ -28152,7 +28342,7 @@ async function authenticateMcpRequest(authHeader, validationClient, mcpPublicUrl
28152
28342
  }
28153
28343
  const token = authHeader.slice(BEARER_PREFIX.length);
28154
28344
  if (!MCP_TOKEN_FORMAT.test(token)) {
28155
- return reject("invalid_token");
28345
+ return reject("invalid_token", { reason: "malformed_credential" });
28156
28346
  }
28157
28347
  const outcome = await validateMcpToken(token, validationClient);
28158
28348
  return decideFromOutcome(token, outcome, mcpPublicUrl2);
@@ -28182,7 +28372,8 @@ function createExpressApp(options2) {
28182
28372
  websiteUrl,
28183
28373
  checkoutBaseUrl: checkoutBaseUrl2,
28184
28374
  allowedMcpOriginHostnames = DEFAULT_ALLOWED_MCP_ORIGIN_HOSTNAMES,
28185
- deploymentRouter: deploymentRouterOption
28375
+ deploymentRouter: deploymentRouterOption,
28376
+ now = () => /* @__PURE__ */ new Date()
28186
28377
  } = options2;
28187
28378
  const deploymentRouter2 = deploymentRouterOption ?? (backendClients2 && hasLiveBackend(backendClients2) ? createDualRouter(backendClients2) : void 0);
28188
28379
  const tokenValidationClient = deploymentRouter2?.tokenValidationClient ?? backendClients2?.live ?? backendClients2?.sandbox;
@@ -28259,17 +28450,17 @@ function createExpressApp(options2) {
28259
28450
  const challengeScope = challengeOAuthScopes(deploymentRouter2).join(" ");
28260
28451
  const mcpOriginMiddleware = createMcpOriginMiddleware(allowedMcpOriginHostnames);
28261
28452
  const connectDiagnosticsUrl = buildMcpConnectDiagnosticsUrl(mcpPath2, mcpPublicUrl2);
28262
- const sendUnauthorized = (res, code, expiredMessage) => {
28453
+ const sendUnauthorized = (res, rejection, expiredMessage) => {
28263
28454
  if (resourceMetadataUrl) {
28264
28455
  res.set(
28265
28456
  "WWW-Authenticate",
28266
28457
  `Bearer realm="PAI Managed MCP", resource_metadata="${resourceMetadataUrl}", scope="${challengeScope}"`
28267
28458
  );
28268
28459
  }
28269
- console.error(formatMcpAuthRejectionLog(code, routerMode));
28460
+ console.error(formatMcpAuthRejectionLog(rejection, routerMode));
28270
28461
  res.status(401).json(
28271
28462
  buildMcpAuthRejectionBody(
28272
- code,
28463
+ rejection.code,
28273
28464
  { serverDeployment, expiredMessage },
28274
28465
  { docsBaseUrl: docsBaseUrl2, connectDiagnosticsUrl }
28275
28466
  )
@@ -28289,7 +28480,7 @@ function createExpressApp(options2) {
28289
28480
  return;
28290
28481
  }
28291
28482
  if (!decision.ok) {
28292
- sendUnauthorized(res, decision.code, decision.expiredMessage);
28483
+ sendUnauthorized(res, decision.rejection, decision.expiredMessage);
28293
28484
  return;
28294
28485
  }
28295
28486
  req.mcpTokenPayload = decision.payload;
@@ -28327,7 +28518,9 @@ function createExpressApp(options2) {
28327
28518
  checkoutBaseUrl: checkoutBaseUrl2,
28328
28519
  dashboardUrl: websiteUrl,
28329
28520
  mcpPublicUrl: mcpPublicUrl2,
28330
- serverDeployment
28521
+ serverDeployment,
28522
+ docsBaseUrl: docsBaseUrl2,
28523
+ now
28331
28524
  });
28332
28525
  registerPrompts(server, {
28333
28526
  servesGoLive: servesGoLive(deploymentRouter2),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payments-ai/mcp",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "private": false,
5
5
  "description": "MCP for Payments AI",
6
6
  "repository": {