@payments-ai/mcp 1.11.1 → 1.12.3

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 +1004 -756
  2. package/package.json +2 -2
package/dist/main.http.js CHANGED
@@ -105,8 +105,23 @@ function normalizeValidatedHttpBaseUrl(trimmed, envName) {
105
105
  const pathname = parsed.pathname.replace(/\/+$/, "");
106
106
  return `${parsed.origin}${pathname}`;
107
107
  }
108
- function readRequiredDualBackendUrlConfig(env = process.env) {
108
+ var MCP_DEPLOYMENT_MODE_VALUES = ["dual", "solo"];
109
+ function readMcpDeploymentMode(env = process.env) {
110
+ const raw = env.MCP_DEPLOYMENT_MODE?.trim();
111
+ if (!raw) {
112
+ return "dual";
113
+ }
114
+ if (!MCP_DEPLOYMENT_MODE_VALUES.includes(raw)) {
115
+ throw new Error(`MCP_DEPLOYMENT_MODE must be one of: ${MCP_DEPLOYMENT_MODE_VALUES.join(", ")}`);
116
+ }
117
+ return raw;
118
+ }
119
+ function readBackendUrlConfig(mode, env = process.env) {
109
120
  const sandboxUrl = readRequiredHttpBaseUrlFromEnv("BACKEND_SANDBOX_URL", env);
121
+ if (mode === "solo") {
122
+ const liveUrl2 = env.BACKEND_LIVE_URL?.trim();
123
+ return liveUrl2 ? { sandboxUrl, liveUrl: normalizeValidatedHttpBaseUrl(liveUrl2, "BACKEND_LIVE_URL") } : { sandboxUrl };
124
+ }
110
125
  const liveUrl = readRequiredHttpBaseUrlFromEnv("BACKEND_LIVE_URL", env);
111
126
  return { sandboxUrl, liveUrl };
112
127
  }
@@ -156,6 +171,9 @@ function readSentryTracesSampleRate(env = process.env) {
156
171
  defaultTracesSampleRateForAppEnv(appEnv)
157
172
  );
158
173
  }
174
+ function isSentryEnabled(env = process.env) {
175
+ return Boolean(readSentryDsn(env));
176
+ }
159
177
  function scrubAuthorizationHeader(headers) {
160
178
  if (!headers) {
161
179
  return headers;
@@ -204,6 +222,15 @@ function buildSentryInitOptions(env = process.env) {
204
222
  var options = buildSentryInitOptions();
205
223
  if (options) {
206
224
  Sentry.init(options);
225
+ console.error(
226
+ JSON.stringify({
227
+ event: "mcp.sentry_initialized",
228
+ environment: options.environment,
229
+ tracesSampleRate: options.tracesSampleRate
230
+ })
231
+ );
232
+ } else {
233
+ console.error(JSON.stringify({ event: "mcp.sentry_disabled" }));
207
234
  }
208
235
 
209
236
  // src/shared/backend-user-error.ts
@@ -240,6 +267,34 @@ function readMcpKeyExpiredMessage(bodyText) {
240
267
  }
241
268
  return void 0;
242
269
  }
270
+ var BUSINESS_NAME_TAKEN_CODE = "business_name_taken";
271
+ var BUSINESS_NAME_TAKEN_RECOVERY = "Recovery: call create_merchant_account with a different unique name. Do not retry with the same name.";
272
+ var DUAL_PROVISION_FINISH_RECOVERY = "Recovery: retry create_merchant_account with the same merchantId, name, and email to finish provisioning without creating a new id.";
273
+ function extractClientErrorCode(bodyText) {
274
+ const trimmed = bodyText.trim();
275
+ if (!trimmed) {
276
+ return void 0;
277
+ }
278
+ try {
279
+ const json2 = JSON.parse(trimmed);
280
+ if (typeof json2.code === "string" && json2.code.trim()) {
281
+ return json2.code.trim();
282
+ }
283
+ } catch {
284
+ return void 0;
285
+ }
286
+ return void 0;
287
+ }
288
+ function formatCreateMerchantAccountBackendFailure(result, options2) {
289
+ const base = formatToolBackendFailure("create your merchant account", result);
290
+ const includeRecovery = options2?.includeRecovery ?? true;
291
+ const code = result.kind === "http_error" ? extractClientErrorCode(result.bodyText ?? "") : void 0;
292
+ if (code !== BUSINESS_NAME_TAKEN_CODE || !includeRecovery) {
293
+ return base;
294
+ }
295
+ return `${base}
296
+ ${BUSINESS_NAME_TAKEN_RECOVERY}`;
297
+ }
243
298
  function extractClientErrorMessage(bodyText) {
244
299
  return withoutProviderBrandName(readClientErrorMessage(bodyText));
245
300
  }
@@ -301,6 +356,7 @@ ${result.message}`;
301
356
  // src/backend/backend-client.ts
302
357
  var BACKEND_HTTP_HEALTH_PATH = "/api/v1/health-checks/http";
303
358
  var BACKEND_CREATE_MERCHANT_ACCOUNT_PATH = "/api/v1/mcp/provision";
359
+ var BACKEND_MCP_MY_MERCHANTS_PATH = "/api/v1/mcp/my-merchants";
304
360
  function backendCreateProductPath(merchantId) {
305
361
  return `/api/v1/mcp/merchants/${merchantId}/products`;
306
362
  }
@@ -341,6 +397,9 @@ function backendConfirmCheckoutLogoPath(merchantId) {
341
397
  return `/api/v1/mcp/merchants/${merchantId}/checkout/logo/confirm`;
342
398
  }
343
399
  var DEFAULT_TIMEOUT_MS = 1e4;
400
+ function hasLiveBackend(clients) {
401
+ return clients.live !== void 0;
402
+ }
344
403
  function normalizeBackendBaseUrl(baseUrl) {
345
404
  return baseUrl.replace(/\/+$/, "");
346
405
  }
@@ -494,10 +553,13 @@ function createBackendClient(baseUrl, options2, env = process.env) {
494
553
  });
495
554
  }
496
555
  function createDualBackendClients(config2, options2, env = process.env) {
497
- return {
498
- sandbox: createBackendClient(config2.sandboxUrl, options2, env),
499
- live: createBackendClient(config2.liveUrl, options2, env)
556
+ const clients = {
557
+ sandbox: createBackendClient(config2.sandboxUrl, options2, env)
500
558
  };
559
+ if (config2.liveUrl) {
560
+ return { ...clients, live: createBackendClient(config2.liveUrl, options2, env) };
561
+ }
562
+ return clients;
501
563
  }
502
564
  function createDualBackendClientsFromEnv(explicit, env = process.env) {
503
565
  const sandboxUrl = explicit?.sandboxUrl?.trim() || resolveBackendSandboxUrl(env);
@@ -511,19 +573,43 @@ function createDualBackendClientsFromEnv(explicit, env = process.env) {
511
573
  return createDualBackendClients({ sandboxUrl, liveUrl }, void 0, env);
512
574
  }
513
575
 
514
- // src/http/app.ts
515
- import cors from "cors";
516
- import { createMcpHandler } from "@modelcontextprotocol/server";
517
- import { toNodeHandler } from "@modelcontextprotocol/node";
518
- import {
519
- createMcpExpressApp,
520
- getOAuthProtectedResourceMetadataUrl,
521
- mcpAuthMetadataRouter
522
- } from "@modelcontextprotocol/express";
576
+ // src/backend/resolve-product-checkout-backend.ts
577
+ function parseGoLiveStatusBody(bodyText) {
578
+ let parsed;
579
+ try {
580
+ parsed = JSON.parse(bodyText);
581
+ } catch {
582
+ return void 0;
583
+ }
584
+ if (typeof parsed !== "object" || parsed === null || !("liveMerchantId" in parsed)) {
585
+ return void 0;
586
+ }
587
+ const liveMerchantId = parsed.liveMerchantId;
588
+ if (liveMerchantId !== null && typeof liveMerchantId !== "string") {
589
+ return void 0;
590
+ }
591
+ if (typeof liveMerchantId === "string" && liveMerchantId.trim() === "") {
592
+ return void 0;
593
+ }
594
+ return { liveMerchantId };
595
+ }
596
+ async function resolveProductCheckoutBackend(clients, merchantId) {
597
+ const result = await clients.sandbox.get(backendGoLiveStatusPath(merchantId));
598
+ if (!result.ok) {
599
+ return { client: clients.sandbox, merchantId, deployment: "sandbox" };
600
+ }
601
+ const status = parseGoLiveStatusBody(result.bodyText);
602
+ if (!status || status.liveMerchantId === null) {
603
+ return { client: clients.sandbox, merchantId, deployment: "sandbox" };
604
+ }
605
+ return { client: clients.live, merchantId: status.liveMerchantId, deployment: "live" };
606
+ }
607
+
608
+ // src/tools/merchant/create-merchant-account.tool.ts
609
+ import * as crypto from "node:crypto";
523
610
 
524
611
  // ../../packages/mcp-contracts/dist/tool-names.js
525
612
  var MCP_TOOL_NAMES = {
526
- PAYMENTS_AI_HEALTH: "payments_ai_health",
527
613
  /** Discovery before creation — see PAIM-973; `create_merchant_account` points at it. */
528
614
  GET_MY_MERCHANT: "get_my_merchant",
529
615
  CREATE_MERCHANT_ACCOUNT: "create_merchant_account",
@@ -542,7 +628,6 @@ var MCP_TOOL_NAMES = {
542
628
 
543
629
  // ../../packages/mcp-contracts/dist/oauth-metadata.js
544
630
  var SUPPORTED_OAUTH_SCOPES = [
545
- "health:read",
546
631
  "merchant:create",
547
632
  "merchant:read",
548
633
  "merchant:go_live",
@@ -552,6 +637,15 @@ var SUPPORTED_OAUTH_SCOPES = [
552
637
  "checkout:read",
553
638
  "checkout:write"
554
639
  ];
640
+ var OAUTH_CONSENT_READ_SCOPES = [
641
+ "merchant:read",
642
+ "product:read",
643
+ "docs:read",
644
+ "checkout:read"
645
+ ];
646
+ var DEFAULT_OAUTH_SCOPES = [
647
+ ...OAUTH_CONSENT_READ_SCOPES
648
+ ];
555
649
  var OAUTH_RESPONSE_TYPES_SUPPORTED = ["code"];
556
650
  var OAUTH_GRANT_TYPES_SUPPORTED = ["authorization_code"];
557
651
  var OAUTH_CODE_CHALLENGE_METHODS_SUPPORTED = ["S256"];
@@ -15290,7 +15384,7 @@ var mcpMerchantSummarySchema = external_exports.object({
15290
15384
  // ../../packages/mcp-contracts/dist/create-merchant-account.js
15291
15385
  var createMerchantAccountInputSchema = external_exports.object({
15292
15386
  name: external_exports.string().trim().min(1, {
15293
- message: "Provide a non-empty business or display name. Recovery: re-send the tool call with a visible label for the merchant account."
15387
+ message: "Provide a non-empty business or display name that is unique across Payments AI. Recovery: pick a new name (for example add your company or a short suffix) and re-send the tool call."
15294
15388
  }),
15295
15389
  email: external_exports.email({
15296
15390
  error: "Provide a valid email. Recovery: check spelling, domain, and that the value is a single RFC-style address."
@@ -15327,8 +15421,17 @@ var activationStatusOutputSchema = external_exports.object({
15327
15421
  var goLiveInputSchema = external_exports.object({
15328
15422
  merchantId: external_exports.string().trim().min(1, {
15329
15423
  message: "Provide the merchant account identifier returned from create_merchant_account. Recovery: copy the merchantId from a prior successful create_merchant_account response (the same id exists on sandbox and live after dual provision)."
15330
- })
15331
- });
15424
+ }),
15425
+ /**
15426
+ * Run the readiness checks and stop before anything is written.
15427
+ *
15428
+ * This exists because an agent facing the only irreversible tool in the set
15429
+ * reaches for the obvious hatch. Without it, `{ dryRun: true }` was accepted
15430
+ * and silently dropped, so the caller believed it had simulated a promotion
15431
+ * that had in fact just happened for real.
15432
+ */
15433
+ dryRun: external_exports.boolean().optional()
15434
+ }).strict();
15332
15435
  var goLiveKycNotifyChannelSchema = external_exports.enum(["sms", "email"]);
15333
15436
  var goLiveKycNotifyOutcomeSchema = external_exports.discriminatedUnion("status", [
15334
15437
  external_exports.object({ status: external_exports.literal("already_dispatched") }),
@@ -15345,7 +15448,13 @@ var goLiveKycNotifyOutcomeSchema = external_exports.discriminatedUnion("status",
15345
15448
  ]);
15346
15449
  var goLiveOutputSchema = external_exports.object({
15347
15450
  liveMerchantId: external_exports.string().min(1),
15348
- kycNotify: goLiveKycNotifyOutcomeSchema.optional()
15451
+ kycNotify: goLiveKycNotifyOutcomeSchema.optional(),
15452
+ /**
15453
+ * Present and `true` only when the call was a dry run. Its absence is the
15454
+ * caller's proof that the promotion actually happened, so it is never emitted
15455
+ * as `false`.
15456
+ */
15457
+ dryRun: external_exports.literal(true).optional()
15349
15458
  });
15350
15459
 
15351
15460
  // ../../packages/mcp-contracts/dist/payments-ai-health.js
@@ -15458,83 +15567,6 @@ var setCheckoutLogoOutputSchema = external_exports.union([
15458
15567
  confirmCheckoutLogoOutputSchema
15459
15568
  ]);
15460
15569
 
15461
- // src/load-package-meta.ts
15462
- import { readFileSync } from "node:fs";
15463
- import { dirname, join } from "node:path";
15464
- import { fileURLToPath } from "node:url";
15465
- var __dirname = dirname(fileURLToPath(import.meta.url));
15466
- function readMcpServerVersion() {
15467
- const path2 = join(__dirname, "..", "package.json");
15468
- const raw = readFileSync(path2, "utf8");
15469
- const pkg = JSON.parse(raw);
15470
- return pkg.version ?? "0.0.0";
15471
- }
15472
-
15473
- // src/observability/create-instrumented-mcp-server.ts
15474
- import { McpServer } from "@modelcontextprotocol/server";
15475
- var MCP_SERVER_NAME = "pai-managed-mcp-server";
15476
- function createInstrumentedMcpServer(options2) {
15477
- const server = new McpServer({
15478
- name: MCP_SERVER_NAME,
15479
- version: options2.version
15480
- });
15481
- return Sentry.wrapMcpServerWithSentry(server, {
15482
- recordInputs: false,
15483
- recordOutputs: false
15484
- });
15485
- }
15486
-
15487
- // src/backend/resolve-product-checkout-backend.ts
15488
- function parseGoLiveStatusBody(bodyText) {
15489
- let parsed;
15490
- try {
15491
- parsed = JSON.parse(bodyText);
15492
- } catch {
15493
- return void 0;
15494
- }
15495
- if (typeof parsed !== "object" || parsed === null || !("liveMerchantId" in parsed)) {
15496
- return void 0;
15497
- }
15498
- const liveMerchantId = parsed.liveMerchantId;
15499
- if (liveMerchantId !== null && typeof liveMerchantId !== "string") {
15500
- return void 0;
15501
- }
15502
- if (typeof liveMerchantId === "string" && liveMerchantId.trim() === "") {
15503
- return void 0;
15504
- }
15505
- return { liveMerchantId };
15506
- }
15507
- async function resolveProductCheckoutBackend(clients, merchantId) {
15508
- const result = await clients.sandbox.get(backendGoLiveStatusPath(merchantId));
15509
- if (!result.ok) {
15510
- return { client: clients.sandbox, merchantId, deployment: "sandbox" };
15511
- }
15512
- const status = parseGoLiveStatusBody(result.bodyText);
15513
- if (!status || status.liveMerchantId === null) {
15514
- return { client: clients.sandbox, merchantId, deployment: "sandbox" };
15515
- }
15516
- return { client: clients.live, merchantId: status.liveMerchantId, deployment: "live" };
15517
- }
15518
-
15519
- // src/tools/scope-denial.ts
15520
- var DEVELOPER_TOOLS_PATH = "/developer-tools";
15521
- function isScopeAllowed(required2, payload) {
15522
- if (!payload) return true;
15523
- if (payload.scopes === null) return true;
15524
- return payload.scopes.includes(required2);
15525
- }
15526
- function insufficientScopeMessage(required2, dashboardUrl) {
15527
- const location = dashboardUrl ? `at ${dashboardUrl.replace(/\/+$/, "")}${DEVELOPER_TOOLS_PATH}` : `under ${DEVELOPER_TOOLS_PATH}`;
15528
- return `Insufficient scope. This tool requires the '${required2}' scope, which this connection does not have. Recovery: re-authorize the connection requesting '${required2}', or mint a new MCP key that includes it in the Payments AI dashboard ${location}.`;
15529
- }
15530
- function scopeDenial(required2, payload, dashboardUrl) {
15531
- if (isScopeAllowed(required2, payload)) return void 0;
15532
- return {
15533
- isError: true,
15534
- content: [{ type: "text", text: insufficientScopeMessage(required2, dashboardUrl) }]
15535
- };
15536
- }
15537
-
15538
15570
  // src/shared/zod-format.ts
15539
15571
  function formatZodIssuesForTool(issues) {
15540
15572
  return issues.map((issue2) => {
@@ -15543,65 +15575,195 @@ function formatZodIssuesForTool(issues) {
15543
15575
  }).join("\n");
15544
15576
  }
15545
15577
 
15546
- // src/tools/merchant/activation-status.tool.ts
15547
- function stubStatusForAccount(merchantId) {
15548
- return merchantId.toLowerCase().includes("pending") ? "pending" : "not_provisioned";
15549
- }
15550
- function buildActivationStatusStub(input) {
15551
- return activationStatusOutputSchema.parse({
15552
- status: stubStatusForAccount(input.merchantId)
15553
- });
15578
+ // src/tools/merchant/create-merchant-account.tool.ts
15579
+ var randomUuidV7 = crypto.randomUUIDv7;
15580
+ var STUB_MERCHANT = {
15581
+ merchantId: "01930000-0000-7000-8000-000000000001"
15582
+ };
15583
+ function buildCreateMerchantAccountStub(_input) {
15584
+ return createMerchantAccountOutputSchema.parse(STUB_MERCHANT);
15554
15585
  }
15555
- function runActivationStatusHandler(raw) {
15556
- const parsed = activationStatusInputSchema.safeParse(raw);
15586
+ function runCreateMerchantAccountHandler(raw) {
15587
+ const parsed = createMerchantAccountInputSchema.safeParse(raw);
15557
15588
  if (!parsed.success) {
15558
15589
  return {
15559
15590
  ok: false,
15560
- errorText: `Invalid arguments for get_merchant_activation_status.
15591
+ errorText: `Invalid arguments for create_merchant_account.
15561
15592
  ${formatZodIssuesForTool(parsed.error.issues)}`
15562
15593
  };
15563
15594
  }
15564
- return { ok: true, data: buildActivationStatusStub(parsed.data) };
15595
+ return { ok: true, data: buildCreateMerchantAccountStub(parsed.data) };
15565
15596
  }
15566
- async function runActivationStatusHandlerWithBackend(backendClient, input) {
15567
- const result = await backendClient.get(backendActivationStatusPath(input.merchantId));
15597
+ function formatDualProvisionPartialFailure(params) {
15598
+ const livePart = params.live.ok ? "live: ok" : `live: ${params.live.errorText}`;
15599
+ const sandboxPart = params.sandbox.ok ? "sandbox: ok" : `sandbox: ${params.sandbox.errorText}`;
15600
+ return `Could not create your merchant account on both backends (partial failure).
15601
+ merchantId: ${params.merchantId}
15602
+ ${livePart}
15603
+ ${sandboxPart}
15604
+ ${formatDualProvisionRecovery(params.live, params.sandbox)}`;
15605
+ }
15606
+ function sideErrorCode(side) {
15607
+ return side.ok ? void 0 : side.code;
15608
+ }
15609
+ function formatDualProvisionRecovery(live, sandbox) {
15610
+ if (live.ok || sandbox.ok) {
15611
+ return DUAL_PROVISION_FINISH_RECOVERY;
15612
+ }
15613
+ if (sideErrorCode(live) === BUSINESS_NAME_TAKEN_CODE || sideErrorCode(sandbox) === BUSINESS_NAME_TAKEN_CODE) {
15614
+ return BUSINESS_NAME_TAKEN_RECOVERY;
15615
+ }
15616
+ return DUAL_PROVISION_FINISH_RECOVERY;
15617
+ }
15618
+ function formatSandboxOnlyProvisionWarning(params) {
15619
+ return `Warning: the sandbox copy of this merchant account was not created.
15620
+ sandbox: ${params.sandboxErrorText}
15621
+ The live merchant account exists and this merchantId is safe to use.
15622
+ Sandbox products and sandbox checkout links stay unavailable until this succeeds.
15623
+ ` + DUAL_PROVISION_FINISH_RECOVERY;
15624
+ }
15625
+ function withMatchingId(side, merchantId) {
15626
+ if (!side.ok || side.merchantId === merchantId) {
15627
+ return side;
15628
+ }
15629
+ return { ok: false, errorText: UNEXPECTED_RESPONSE_ERROR };
15630
+ }
15631
+ function logDualProvisionOutcome(params) {
15632
+ console.error("[create_merchant_account] dual-provision", {
15633
+ merchantId: params.merchantId,
15634
+ outcome: params.outcome,
15635
+ live: params.live.ok ? "ok" : "error",
15636
+ sandbox: params.sandbox.ok ? "ok" : "error",
15637
+ liveError: params.live.ok ? void 0 : params.live.errorText,
15638
+ sandboxError: params.sandbox.ok ? void 0 : params.sandbox.errorText
15639
+ });
15640
+ }
15641
+ async function provisionOnBackend(backendClient, input, merchantId, identity) {
15642
+ const result = await backendClient.post(BACKEND_CREATE_MERCHANT_ACCOUNT_PATH, {
15643
+ name: input.name,
15644
+ email: input.email,
15645
+ ...input.phone ? { phone: input.phone } : {},
15646
+ merchantId,
15647
+ ...identity?.userId ? { userId: identity.userId } : {},
15648
+ ...identity?.cognitoSub ? { cognitoSub: identity.cognitoSub } : {}
15649
+ });
15568
15650
  if (!result.ok) {
15569
15651
  return {
15570
15652
  ok: false,
15571
- errorText: formatToolBackendFailure("load merchant activation status", result)
15653
+ errorText: formatCreateMerchantAccountBackendFailure(result, { includeRecovery: false }),
15654
+ code: result.kind === "http_error" ? extractClientErrorCode(result.bodyText ?? "") : void 0
15572
15655
  };
15573
15656
  }
15574
15657
  let parsedJson;
15575
15658
  try {
15576
15659
  parsedJson = JSON.parse(result.bodyText);
15577
15660
  } catch {
15578
- return {
15579
- ok: false,
15580
- errorText: UNEXPECTED_RESPONSE_ERROR
15581
- };
15661
+ return { ok: false, errorText: UNEXPECTED_RESPONSE_ERROR };
15582
15662
  }
15583
- const out = activationStatusOutputSchema.safeParse(parsedJson);
15663
+ const out = createMerchantAccountOutputSchema.safeParse(parsedJson);
15584
15664
  if (!out.success) {
15585
- return {
15586
- ok: false,
15587
- errorText: UNEXPECTED_RESPONSE_ERROR
15588
- };
15665
+ return { ok: false, errorText: UNEXPECTED_RESPONSE_ERROR };
15589
15666
  }
15590
- return { ok: true, data: out.data };
15591
- }
15592
-
15593
- // src/tools/merchant/go-live.tool.ts
15594
- var goLiveStatusBodySchema = external_exports.object({
15595
- liveMerchantId: external_exports.string().nullable()
15596
- });
15597
- var CONFLICT_LIVE_ID_ERROR = "This sandbox merchant is already linked to a different live merchant. Recovery: use the existing liveMerchantId from go-live-status, or contact support \u2014 Merchant Go-Live is one-way and cannot overwrite a conflicting link.";
15598
- var LIVE_TWIN_MISSING_ERROR = "The live twin merchant was not found. Recovery: ensure create_merchant_account finished dual provision on both backends (same merchantId on live and sandbox), then retry go_live.";
15599
- var SANDBOX_MISSING_ERROR = "The sandbox merchant was not found. Recovery: create the merchant with create_merchant_account first, then retry go_live.";
15600
- function buildGoLiveStub(input) {
15601
- return goLiveOutputSchema.parse({
15602
- liveMerchantId: input.merchantId,
15603
- kycNotify: { status: "skipped", reason: "stub_backends" }
15604
- });
15667
+ return { ok: true, merchantId: out.data.merchantId };
15668
+ }
15669
+ async function runCreateMerchantAccountHandlerWithDualBackend(clients, input, identity) {
15670
+ const merchantId = input.merchantId ?? randomUuidV7();
15671
+ const sharedOrigin = clients.live.getBaseUrl() === clients.sandbox.getBaseUrl();
15672
+ if (sharedOrigin) {
15673
+ const side = await provisionOnBackend(clients.live, input, merchantId, identity);
15674
+ if (!side.ok) {
15675
+ const failure = side;
15676
+ logDualProvisionOutcome({
15677
+ merchantId,
15678
+ live: failure,
15679
+ sandbox: failure,
15680
+ outcome: "total_failure"
15681
+ });
15682
+ return {
15683
+ ok: false,
15684
+ errorText: formatDualProvisionPartialFailure({
15685
+ merchantId,
15686
+ live: failure,
15687
+ sandbox: failure
15688
+ })
15689
+ };
15690
+ }
15691
+ const okSide = { ok: true, merchantId };
15692
+ logDualProvisionOutcome({
15693
+ merchantId,
15694
+ live: okSide,
15695
+ sandbox: okSide,
15696
+ outcome: "success"
15697
+ });
15698
+ return { ok: true, data: { merchantId } };
15699
+ }
15700
+ const [liveSettled, sandboxSettled] = await Promise.allSettled([
15701
+ provisionOnBackend(clients.live, input, merchantId, { userId: identity?.userId }),
15702
+ provisionOnBackend(clients.sandbox, input, merchantId, { cognitoSub: identity?.cognitoSub })
15703
+ ]);
15704
+ const live = liveSettled.status === "fulfilled" ? liveSettled.value : {
15705
+ ok: false,
15706
+ errorText: liveSettled.reason instanceof Error ? liveSettled.reason.message : String(liveSettled.reason)
15707
+ };
15708
+ const sandbox = sandboxSettled.status === "fulfilled" ? sandboxSettled.value : {
15709
+ ok: false,
15710
+ errorText: sandboxSettled.reason instanceof Error ? sandboxSettled.reason.message : String(sandboxSettled.reason)
15711
+ };
15712
+ const liveSide = withMatchingId(live, merchantId);
15713
+ const sandboxSide = withMatchingId(sandbox, merchantId);
15714
+ if (liveSide.ok && sandboxSide.ok) {
15715
+ logDualProvisionOutcome({
15716
+ merchantId,
15717
+ live: liveSide,
15718
+ sandbox: sandboxSide,
15719
+ outcome: "success"
15720
+ });
15721
+ return { ok: true, data: { merchantId } };
15722
+ }
15723
+ const outcome = !liveSide.ok && !sandboxSide.ok ? "total_failure" : "partial_failure";
15724
+ logDualProvisionOutcome({ merchantId, live: liveSide, sandbox: sandboxSide, outcome });
15725
+ if (liveSide.ok && !sandboxSide.ok) {
15726
+ return {
15727
+ ok: true,
15728
+ data: { merchantId },
15729
+ warningText: formatSandboxOnlyProvisionWarning({
15730
+ sandboxErrorText: sandboxSide.errorText
15731
+ })
15732
+ };
15733
+ }
15734
+ return {
15735
+ ok: false,
15736
+ errorText: formatDualProvisionPartialFailure({
15737
+ merchantId,
15738
+ live: liveSide,
15739
+ sandbox: sandboxSide
15740
+ })
15741
+ };
15742
+ }
15743
+ async function runCreateMerchantAccountHandlerWithBackend(backendClient, input, identity) {
15744
+ const merchantId = input.merchantId ?? randomUuidV7();
15745
+ const side = await provisionOnBackend(backendClient, input, merchantId, identity);
15746
+ if (!side.ok) {
15747
+ const errorText = side.code === BUSINESS_NAME_TAKEN_CODE ? `${side.errorText}
15748
+ ${BUSINESS_NAME_TAKEN_RECOVERY}` : side.errorText;
15749
+ return { ok: false, errorText };
15750
+ }
15751
+ return { ok: true, data: { merchantId: side.merchantId } };
15752
+ }
15753
+
15754
+ // src/tools/merchant/go-live.tool.ts
15755
+ var goLiveStatusBodySchema = external_exports.object({
15756
+ liveMerchantId: external_exports.string().nullable()
15757
+ });
15758
+ var CONFLICT_LIVE_ID_ERROR = "This sandbox merchant is already linked to a different live merchant. Recovery: use the existing liveMerchantId from go-live-status, or contact support \u2014 Merchant Go-Live is one-way and cannot overwrite a conflicting link.";
15759
+ var LIVE_TWIN_MISSING_ERROR = "The live twin merchant was not found. Recovery: ensure create_merchant_account finished dual provision on both backends (same merchantId on live and sandbox), then retry go_live.";
15760
+ var SANDBOX_MISSING_ERROR = "The sandbox merchant was not found. Recovery: create the merchant with create_merchant_account first, then retry go_live.";
15761
+ function buildGoLiveStub(input) {
15762
+ return goLiveOutputSchema.parse({
15763
+ liveMerchantId: input.merchantId,
15764
+ kycNotify: { status: "skipped", reason: input.dryRun === true ? "dry_run" : "stub_backends" },
15765
+ ...input.dryRun === true ? { dryRun: true } : {}
15766
+ });
15605
15767
  }
15606
15768
  function runGoLiveHandler(raw) {
15607
15769
  const parsed = goLiveInputSchema.safeParse(raw);
@@ -15688,6 +15850,17 @@ async function runGoLiveHandlerWithDualBackend(clients, input, userId) {
15688
15850
  logGoLiveOutcome({ merchantId: twinId, outcome: "unexpected_response", userId });
15689
15851
  return { ok: false, errorText: UNEXPECTED_RESPONSE_ERROR };
15690
15852
  }
15853
+ if (input.dryRun === true) {
15854
+ logGoLiveOutcome({ merchantId: twinId, outcome: "dry_run", userId });
15855
+ return {
15856
+ ok: true,
15857
+ data: goLiveOutputSchema.parse({
15858
+ liveMerchantId: twinId,
15859
+ kycNotify: { status: "skipped", reason: "dry_run" },
15860
+ dryRun: true
15861
+ })
15862
+ };
15863
+ }
15691
15864
  const markLive = await clients.sandbox.post(backendGoLivePath(twinId), {
15692
15865
  liveMerchantId: twinId
15693
15866
  });
@@ -15710,42 +15883,99 @@ async function runGoLiveHandlerWithDualBackend(clients, input, userId) {
15710
15883
  };
15711
15884
  }
15712
15885
 
15713
- // src/tools/product/build-plan-checkout-url.ts
15714
- var CHECKOUT_SANDBOX_QUERY_PARAM = "isSandbox";
15715
- function buildPlanCheckoutUrl(params) {
15716
- const normalizedBaseUrl = params.checkoutBaseUrl.replace(/\/+$/, "");
15717
- const url2 = new URL(`${normalizedBaseUrl}/payment/${params.planId}`);
15718
- if (params.deployment === "sandbox") {
15719
- url2.searchParams.set(CHECKOUT_SANDBOX_QUERY_PARAM, "true");
15886
+ // src/tools/merchant/get-my-merchant.tool.ts
15887
+ var MERCHANT_LOOKUP_ACTION = "list the merchant accounts for this connection";
15888
+ async function readMerchantName(live, merchantId) {
15889
+ const result = await live.get(backendMerchantPath(merchantId));
15890
+ if (!result.ok) {
15891
+ return { ok: false, errorText: formatToolBackendFailure(MERCHANT_LOOKUP_ACTION, result) };
15720
15892
  }
15721
- return url2.toString();
15893
+ let parsedJson;
15894
+ try {
15895
+ parsedJson = JSON.parse(result.bodyText);
15896
+ } catch {
15897
+ return { ok: false, errorText: UNEXPECTED_RESPONSE_ERROR };
15898
+ }
15899
+ const out = mcpMerchantSummarySchema.safeParse(parsedJson);
15900
+ if (!out.success) {
15901
+ return { ok: false, errorText: UNEXPECTED_RESPONSE_ERROR };
15902
+ }
15903
+ return { ok: true, name: out.data.name };
15904
+ }
15905
+ async function runGetMyMerchantHandlerWithDualBackend(clients, merchantIds) {
15906
+ if (merchantIds.length === 0) {
15907
+ return { ok: true, data: getMyMerchantOutputSchema.parse({ merchants: [] }) };
15908
+ }
15909
+ const resolved = await Promise.all(
15910
+ merchantIds.map(async (merchantId) => {
15911
+ const [name, route] = await Promise.all([
15912
+ readMerchantName(clients.live, merchantId),
15913
+ resolveProductCheckoutBackend(clients, merchantId)
15914
+ ]);
15915
+ if (!name.ok) {
15916
+ return { errorText: name.errorText };
15917
+ }
15918
+ return { merchantId, name: name.name, deployment: route.deployment };
15919
+ })
15920
+ );
15921
+ const failure = resolved.find((entry) => "errorText" in entry);
15922
+ if (failure) {
15923
+ return { ok: false, errorText: failure.errorText };
15924
+ }
15925
+ return {
15926
+ ok: true,
15927
+ data: getMyMerchantOutputSchema.parse({ merchants: resolved })
15928
+ };
15929
+ }
15930
+ async function runGetMyMerchantHandlerSolo(clients, merchantIds) {
15931
+ if (merchantIds.length === 0) {
15932
+ return { ok: true, data: getMyMerchantOutputSchema.parse({ merchants: [] }) };
15933
+ }
15934
+ const resolved = await Promise.all(
15935
+ merchantIds.map(async (merchantId) => {
15936
+ const name = await readMerchantName(clients.sandbox, merchantId);
15937
+ if (!name.ok) {
15938
+ return { errorText: name.errorText };
15939
+ }
15940
+ return { merchantId, name: name.name, deployment: "sandbox" };
15941
+ })
15942
+ );
15943
+ const failure = resolved.find((entry) => "errorText" in entry);
15944
+ if (failure) {
15945
+ return { ok: false, errorText: failure.errorText };
15946
+ }
15947
+ return {
15948
+ ok: true,
15949
+ data: getMyMerchantOutputSchema.parse({ merchants: resolved })
15950
+ };
15722
15951
  }
15723
15952
 
15724
- // src/tools/product/create-product.tool.ts
15725
- var backendCreatedProductShapeSchema = external_exports.object({
15726
- id: external_exports.string(),
15727
- planIds: external_exports.array(external_exports.string())
15728
- });
15729
- var backendCreateProductResponseSchema = backendCreatedProductShapeSchema.extend({
15730
- planIds: external_exports.array(external_exports.string()).min(1)
15731
- });
15732
- function productCreatedWithoutPlansError(productId) {
15733
- return `The product was created (id: ${productId}) but the backend returned no plans for it. Do not retry create_product \u2014 retrying would create a duplicate product. Tell the user the product was created without plans and ask them to review it in the dashboard.`;
15953
+ // src/tools/merchant/activation-status.tool.ts
15954
+ function stubStatusForAccount(merchantId) {
15955
+ return merchantId.toLowerCase().includes("pending") ? "pending" : "not_provisioned";
15734
15956
  }
15735
- function describeCreateProductParseFailure(parsedJson) {
15736
- const loose = backendCreatedProductShapeSchema.safeParse(parsedJson);
15737
- if (loose.success && loose.data.planIds.length === 0) {
15738
- return productCreatedWithoutPlansError(loose.data.id);
15957
+ function buildActivationStatusStub(input) {
15958
+ return activationStatusOutputSchema.parse({
15959
+ status: stubStatusForAccount(input.merchantId)
15960
+ });
15961
+ }
15962
+ function runActivationStatusHandler(raw) {
15963
+ const parsed = activationStatusInputSchema.safeParse(raw);
15964
+ if (!parsed.success) {
15965
+ return {
15966
+ ok: false,
15967
+ errorText: `Invalid arguments for get_merchant_activation_status.
15968
+ ${formatZodIssuesForTool(parsed.error.issues)}`
15969
+ };
15739
15970
  }
15740
- return UNEXPECTED_RESPONSE_ERROR;
15971
+ return { ok: true, data: buildActivationStatusStub(parsed.data) };
15741
15972
  }
15742
- async function runCreateProductHandler(backendClient, input, checkout) {
15743
- const { merchantId, ...body } = input;
15744
- const result = await backendClient.post(backendCreateProductPath(merchantId), body);
15973
+ async function runActivationStatusHandlerWithBackend(backendClient, input) {
15974
+ const result = await backendClient.get(backendActivationStatusPath(input.merchantId));
15745
15975
  if (!result.ok) {
15746
15976
  return {
15747
15977
  ok: false,
15748
- errorText: formatToolBackendFailure("create this product", result)
15978
+ errorText: formatToolBackendFailure("load merchant activation status", result)
15749
15979
  };
15750
15980
  }
15751
15981
  let parsedJson;
@@ -15757,54 +15987,295 @@ async function runCreateProductHandler(backendClient, input, checkout) {
15757
15987
  errorText: UNEXPECTED_RESPONSE_ERROR
15758
15988
  };
15759
15989
  }
15760
- const backendResult = backendCreateProductResponseSchema.safeParse(parsedJson);
15761
- if (!backendResult.success) {
15762
- return {
15763
- ok: false,
15764
- errorText: describeCreateProductParseFailure(parsedJson)
15765
- };
15766
- }
15767
- const output = enrichWithCheckoutUrls(backendResult.data, checkout);
15768
- const validated = createMcpProductOutputSchema.safeParse(output);
15769
- if (!validated.success) {
15990
+ const out = activationStatusOutputSchema.safeParse(parsedJson);
15991
+ if (!out.success) {
15770
15992
  return {
15771
15993
  ok: false,
15772
15994
  errorText: UNEXPECTED_RESPONSE_ERROR
15773
15995
  };
15774
15996
  }
15775
- return { ok: true, data: validated.data };
15997
+ return { ok: true, data: out.data };
15776
15998
  }
15777
- function enrichWithCheckoutUrls(backendResponse, checkout) {
15778
- const planCheckoutUrls = backendResponse.planIds.map((planId) => ({
15779
- planId,
15780
- checkoutUrl: buildPlanCheckoutUrl({
15781
- checkoutBaseUrl: checkout.checkoutBaseUrl,
15782
- planId,
15783
- deployment: checkout.deployment
15784
- })
15785
- }));
15786
- return {
15787
- id: backendResponse.id,
15788
- planIds: backendResponse.planIds,
15789
- checkoutUrl: planCheckoutUrls[0]?.checkoutUrl ?? "",
15790
- planCheckoutUrls
15791
- };
15999
+
16000
+ // src/backend/mcp-deployment-router.ts
16001
+ function missingClientsError() {
16002
+ return { error: "The PAI Managed backend is not reachable. Contact Payments AI support." };
15792
16003
  }
15793
- function runCreateProductHandlerValidation(raw) {
15794
- const parsed = createMcpProductInputSchema.safeParse(raw);
15795
- if (!parsed.success) {
15796
- return {
15797
- ok: false,
15798
- errorText: `Invalid arguments for create_product.
15799
- ${formatZodIssuesForTool(parsed.error.issues)}`
15800
- };
16004
+ function missingClientsResult() {
16005
+ return { ok: false, errorText: missingClientsError().error };
16006
+ }
16007
+ function parseMyMerchantsBody(bodyText) {
16008
+ let parsed;
16009
+ try {
16010
+ parsed = JSON.parse(bodyText);
16011
+ } catch {
16012
+ return void 0;
15801
16013
  }
15802
- return { ok: true, data: parsed.data };
16014
+ if (typeof parsed !== "object" || parsed === null || !("merchantIds" in parsed)) {
16015
+ return void 0;
16016
+ }
16017
+ const merchantIds = parsed.merchantIds;
16018
+ if (!Array.isArray(merchantIds) || !merchantIds.every((id) => typeof id === "string")) {
16019
+ return void 0;
16020
+ }
16021
+ return merchantIds;
15803
16022
  }
15804
-
15805
- // src/tools/product/get-product-image-upload-url.tool.ts
15806
- function runGetProductImageUploadUrlValidation(raw) {
15807
- const parsed = productImageUploadUrlInputSchema.safeParse(raw);
16023
+ function createDualRouter(clients) {
16024
+ return {
16025
+ mode: "dual",
16026
+ supportsGoLive: true,
16027
+ tokenValidationClient: clients.live,
16028
+ healthProbeClient: clients.live,
16029
+ healthProbeLabel: "live backend",
16030
+ async resolveProductCheckout(c, merchantId) {
16031
+ if (!hasLiveBackend(c)) {
16032
+ return missingClientsError();
16033
+ }
16034
+ return resolveProductCheckoutBackend(c, merchantId);
16035
+ },
16036
+ async createMerchantAccount(c, input, identity) {
16037
+ if (!hasLiveBackend(c)) {
16038
+ return missingClientsResult();
16039
+ }
16040
+ return runCreateMerchantAccountHandlerWithDualBackend(c, input, identity);
16041
+ },
16042
+ async goLive(c, input, userId) {
16043
+ if (!hasLiveBackend(c)) {
16044
+ return missingClientsResult();
16045
+ }
16046
+ return runGoLiveHandlerWithDualBackend(c, input, userId);
16047
+ },
16048
+ async getMyMerchant(c, merchantIds) {
16049
+ if (!hasLiveBackend(c)) {
16050
+ return missingClientsResult();
16051
+ }
16052
+ return runGetMyMerchantHandlerWithDualBackend(c, merchantIds);
16053
+ },
16054
+ async activationStatus(c, input) {
16055
+ if (!hasLiveBackend(c)) {
16056
+ return missingClientsResult();
16057
+ }
16058
+ return runActivationStatusHandlerWithBackend(c.live, input);
16059
+ },
16060
+ checkoutLogoClient(c) {
16061
+ return c.sandbox;
16062
+ },
16063
+ async resolveMerchantAllowlist(payload) {
16064
+ return payload?.merchantIds;
16065
+ }
16066
+ };
16067
+ }
16068
+ function createSoloRouter(clients) {
16069
+ return {
16070
+ mode: "solo",
16071
+ supportsGoLive: false,
16072
+ tokenValidationClient: clients.sandbox,
16073
+ healthProbeClient: clients.sandbox,
16074
+ healthProbeLabel: "sandbox backend",
16075
+ async resolveProductCheckout(c, merchantId) {
16076
+ if (!c.sandbox) {
16077
+ return missingClientsError();
16078
+ }
16079
+ return { client: c.sandbox, merchantId, deployment: "sandbox" };
16080
+ },
16081
+ async createMerchantAccount(c, input, identity) {
16082
+ return runCreateMerchantAccountHandlerWithBackend(c.sandbox, input, {
16083
+ cognitoSub: identity?.cognitoSub
16084
+ });
16085
+ },
16086
+ async goLive() {
16087
+ return { ok: false, errorText: "go_live is not available on the sandbox MCP URL." };
16088
+ },
16089
+ async getMyMerchant(c, merchantIds) {
16090
+ return runGetMyMerchantHandlerSolo(c, merchantIds);
16091
+ },
16092
+ async activationStatus(c, input) {
16093
+ return runActivationStatusHandlerWithBackend(c.sandbox, input);
16094
+ },
16095
+ checkoutLogoClient(c) {
16096
+ return c.sandbox;
16097
+ },
16098
+ async resolveMerchantAllowlist(payload) {
16099
+ if (!payload) {
16100
+ return void 0;
16101
+ }
16102
+ const path2 = `${BACKEND_MCP_MY_MERCHANTS_PATH}?cognitoSub=${encodeURIComponent(payload.cognitoSub)}`;
16103
+ const result = await clients.sandbox.get(path2);
16104
+ if (!result.ok) {
16105
+ return {
16106
+ error: "Could not load your merchant list from the sandbox backend. Try again or contact Payments AI support."
16107
+ };
16108
+ }
16109
+ const merchantIds = parseMyMerchantsBody(result.bodyText);
16110
+ if (merchantIds === void 0) {
16111
+ return {
16112
+ error: "Received an invalid merchant list from the sandbox backend. Contact Payments AI support."
16113
+ };
16114
+ }
16115
+ return merchantIds;
16116
+ }
16117
+ };
16118
+ }
16119
+ function createMcpDeploymentRouter(clients, mode) {
16120
+ if (mode === "solo") {
16121
+ return createSoloRouter(clients);
16122
+ }
16123
+ if (!hasLiveBackend(clients)) {
16124
+ throw new Error("BACKEND_LIVE_URL is required for dual MCP deployment");
16125
+ }
16126
+ return createDualRouter(clients);
16127
+ }
16128
+
16129
+ // src/http/app.ts
16130
+ import cors from "cors";
16131
+ import { createMcpHandler } from "@modelcontextprotocol/server";
16132
+ import { toNodeHandler } from "@modelcontextprotocol/node";
16133
+ import {
16134
+ createMcpExpressApp,
16135
+ getOAuthProtectedResourceMetadataUrl as getOAuthProtectedResourceMetadataUrl2,
16136
+ mcpAuthMetadataRouter
16137
+ } from "@modelcontextprotocol/express";
16138
+
16139
+ // src/load-package-meta.ts
16140
+ import { readFileSync } from "node:fs";
16141
+ import { dirname, join } from "node:path";
16142
+ import { fileURLToPath } from "node:url";
16143
+ var __dirname = dirname(fileURLToPath(import.meta.url));
16144
+ function readMcpServerVersion() {
16145
+ const path2 = join(__dirname, "..", "package.json");
16146
+ const raw = readFileSync(path2, "utf8");
16147
+ const pkg = JSON.parse(raw);
16148
+ return pkg.version ?? "0.0.0";
16149
+ }
16150
+
16151
+ // src/observability/create-instrumented-mcp-server.ts
16152
+ import { McpServer } from "@modelcontextprotocol/server";
16153
+ var MCP_SERVER_NAME = "pai-managed-mcp-server";
16154
+ function createInstrumentedMcpServer(options2) {
16155
+ const server = new McpServer({
16156
+ name: MCP_SERVER_NAME,
16157
+ version: options2.version
16158
+ });
16159
+ return Sentry.wrapMcpServerWithSentry(server, {
16160
+ recordInputs: false,
16161
+ recordOutputs: false
16162
+ });
16163
+ }
16164
+
16165
+ // src/tools/scope-denial.ts
16166
+ var DEVELOPER_TOOLS_PATH = "/developer-tools";
16167
+ function isScopeAllowed(required2, payload) {
16168
+ if (!payload) return true;
16169
+ if (payload.scopes === null) return true;
16170
+ return payload.scopes.includes(required2);
16171
+ }
16172
+ function insufficientScopeMessage(required2, dashboardUrl) {
16173
+ const location = dashboardUrl ? `at ${dashboardUrl.replace(/\/+$/, "")}${DEVELOPER_TOOLS_PATH}` : `under ${DEVELOPER_TOOLS_PATH}`;
16174
+ return `Insufficient scope. This tool requires the '${required2}' scope, which this connection does not have. Recovery: re-authorize the connection requesting '${required2}', or mint a new MCP key that includes it in the Payments AI dashboard ${location}.`;
16175
+ }
16176
+ function scopeDenial(required2, payload, dashboardUrl) {
16177
+ if (isScopeAllowed(required2, payload)) return void 0;
16178
+ return {
16179
+ isError: true,
16180
+ content: [{ type: "text", text: insufficientScopeMessage(required2, dashboardUrl) }]
16181
+ };
16182
+ }
16183
+
16184
+ // src/tools/product/build-plan-checkout-url.ts
16185
+ var CHECKOUT_SANDBOX_QUERY_PARAM = "isSandbox";
16186
+ function buildPlanCheckoutUrl(params) {
16187
+ const normalizedBaseUrl = params.checkoutBaseUrl.replace(/\/+$/, "");
16188
+ const url2 = new URL(`${normalizedBaseUrl}/payment/${params.planId}`);
16189
+ if (params.deployment === "sandbox") {
16190
+ url2.searchParams.set(CHECKOUT_SANDBOX_QUERY_PARAM, "true");
16191
+ }
16192
+ return url2.toString();
16193
+ }
16194
+
16195
+ // src/tools/product/create-product.tool.ts
16196
+ var backendCreatedProductShapeSchema = external_exports.object({
16197
+ id: external_exports.string(),
16198
+ planIds: external_exports.array(external_exports.string())
16199
+ });
16200
+ var backendCreateProductResponseSchema = backendCreatedProductShapeSchema.extend({
16201
+ planIds: external_exports.array(external_exports.string()).min(1)
16202
+ });
16203
+ function productCreatedWithoutPlansError(productId) {
16204
+ return `The product was created (id: ${productId}) but the backend returned no plans for it. Do not retry create_product \u2014 retrying would create a duplicate product. Tell the user the product was created without plans and ask them to review it in the dashboard.`;
16205
+ }
16206
+ function describeCreateProductParseFailure(parsedJson) {
16207
+ const loose = backendCreatedProductShapeSchema.safeParse(parsedJson);
16208
+ if (loose.success && loose.data.planIds.length === 0) {
16209
+ return productCreatedWithoutPlansError(loose.data.id);
16210
+ }
16211
+ return UNEXPECTED_RESPONSE_ERROR;
16212
+ }
16213
+ async function runCreateProductHandler(backendClient, input, checkout) {
16214
+ const { merchantId, ...body } = input;
16215
+ const result = await backendClient.post(backendCreateProductPath(merchantId), body);
16216
+ if (!result.ok) {
16217
+ return {
16218
+ ok: false,
16219
+ errorText: formatToolBackendFailure("create this product", result)
16220
+ };
16221
+ }
16222
+ let parsedJson;
16223
+ try {
16224
+ parsedJson = JSON.parse(result.bodyText);
16225
+ } catch {
16226
+ return {
16227
+ ok: false,
16228
+ errorText: UNEXPECTED_RESPONSE_ERROR
16229
+ };
16230
+ }
16231
+ const backendResult = backendCreateProductResponseSchema.safeParse(parsedJson);
16232
+ if (!backendResult.success) {
16233
+ return {
16234
+ ok: false,
16235
+ errorText: describeCreateProductParseFailure(parsedJson)
16236
+ };
16237
+ }
16238
+ const output = enrichWithCheckoutUrls(backendResult.data, checkout);
16239
+ const validated = createMcpProductOutputSchema.safeParse(output);
16240
+ if (!validated.success) {
16241
+ return {
16242
+ ok: false,
16243
+ errorText: UNEXPECTED_RESPONSE_ERROR
16244
+ };
16245
+ }
16246
+ return { ok: true, data: validated.data };
16247
+ }
16248
+ function enrichWithCheckoutUrls(backendResponse, checkout) {
16249
+ const planCheckoutUrls = backendResponse.planIds.map((planId) => ({
16250
+ planId,
16251
+ checkoutUrl: buildPlanCheckoutUrl({
16252
+ checkoutBaseUrl: checkout.checkoutBaseUrl,
16253
+ planId,
16254
+ deployment: checkout.deployment
16255
+ })
16256
+ }));
16257
+ return {
16258
+ id: backendResponse.id,
16259
+ planIds: backendResponse.planIds,
16260
+ checkoutUrl: planCheckoutUrls[0]?.checkoutUrl ?? "",
16261
+ planCheckoutUrls
16262
+ };
16263
+ }
16264
+ function runCreateProductHandlerValidation(raw) {
16265
+ const parsed = createMcpProductInputSchema.safeParse(raw);
16266
+ if (!parsed.success) {
16267
+ return {
16268
+ ok: false,
16269
+ errorText: `Invalid arguments for create_product.
16270
+ ${formatZodIssuesForTool(parsed.error.issues)}`
16271
+ };
16272
+ }
16273
+ return { ok: true, data: parsed.data };
16274
+ }
16275
+
16276
+ // src/tools/product/get-product-image-upload-url.tool.ts
16277
+ function runGetProductImageUploadUrlValidation(raw) {
16278
+ const parsed = productImageUploadUrlInputSchema.safeParse(raw);
15808
16279
  if (!parsed.success) {
15809
16280
  return {
15810
16281
  ok: false,
@@ -15989,422 +16460,177 @@ async function runListProductsHandler(backendClient, input) {
15989
16460
  try {
15990
16461
  parsedJson = JSON.parse(result.bodyText);
15991
16462
  } catch {
15992
- return {
15993
- ok: false,
15994
- errorText: UNEXPECTED_RESPONSE_ERROR
15995
- };
15996
- }
15997
- const mapped = mapBackendListProductsResponse(parsedJson);
15998
- const out = listProductsOutputSchema.safeParse(mapped);
15999
- if (!out.success) {
16000
- return {
16001
- ok: false,
16002
- errorText: UNEXPECTED_RESPONSE_ERROR
16003
- };
16004
- }
16005
- return { ok: true, data: out.data };
16006
- }
16007
-
16008
- // src/tools/docs/knowledge/products-plans.ts
16009
- var DOC_SECTIONS = [
16010
- {
16011
- title: "Navigation \u2014 Products & Plans",
16012
- path: "/docs/products-plans#navigation",
16013
- content: `To access Products & Plans, find **Products** in the left sidebar under the "Product" group (Package icon). Click **Products** to navigate to /products.`
16014
- },
16015
- {
16016
- title: "Create a Product",
16017
- path: "/docs/products-plans#create-a-product",
16018
- content: `1. Call **list_products** with merchantId to discover existing products and planIds (optional filters: status, q, page, limit).
16019
- 2. Call **create_product** with merchantId, name, and at least one plan when you need a new product.
16020
- 3. Optionally attach an image via the presigned upload flow: call **get_product_image_upload_url** with the returned product id, POST the raw bytes (multipart/form-data with the returned fields + file last) to the returned URL, then call **confirm_product_image** with the returned key.
16021
- 4. Use the returned **checkoutUrl** (or **planCheckoutUrls[i].checkoutUrl**) directly \u2014 it already targets the right environment (sandbox URLs carry \`?isSandbox=true\`, live URLs have no query param). Do not construct the URL yourself.`
16022
- },
16023
- {
16024
- title: "Upload a Product Image (MCP)",
16025
- path: "/docs/products-plans#upload-product-image-mcp",
16026
- content: `After create_product returns an id:
16027
- 1. Call **get_product_image_upload_url** with merchantId, productId, fileName, and mimeType. It returns { url, fields, key, expiresIn }.
16028
- 2. HTTP POST the raw file bytes to \`url\` as \`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). Example: \`curl -X POST -F "key=<fields.key>" -F "Content-Type=<mimeType>" -F "policy=<fields.policy>" -F "x-amz-signature=<fields['x-amz-signature']>" ... -F "file=@./logo.png" "<url>"\`.
16029
- 3. Call **confirm_product_image** with merchantId, productId, and the returned key to validate and attach the image.
16030
- 4. Supported formats: PNG, JPEG, WebP. Maximum size: 5 MB (enforced by S3 at upload time via a \`content-length-range\` policy condition \u2014 oversized bodies return a 403 from S3, before the backend ever sees them).
16031
- 5. If confirm fails, retry get_product_image_upload_url + POST + confirm_product_image with the same productId \u2014 the product row is kept; the previous rejected upload was cleaned up by the backend.`
16032
- },
16033
- {
16034
- title: "List Products (MCP)",
16035
- path: "/docs/products-plans#list-products-mcp",
16036
- content: `Call **list_products** with merchantId to list existing products and plans for a merchant. Returns paginated data with planId, name, amount, type, billing period, and checkoutUrl per plan. Optional filters: q (search by name), status (draft|active|archived), type (one-time|recurring|free-access), dateRangeStart/dateRangeEnd, page, limit. Use returned planId or checkoutUrl to build payment links \u2014 see Checkout URL docs. Targets sandbox until go_live.`
16037
- },
16038
- {
16039
- title: "Plan Fields",
16040
- path: "/docs/products-plans#plan-fields",
16041
- content: `Each plan has the following fields:
16042
- - **Name** \u2014 display name for the plan (max 30 characters)
16043
- - **Amount** \u2014 price in USD as a decimal dollar amount (e.g. 9.99 = $9.99)
16044
- - **Type** \u2014 one of: \`one-time\`, \`recurring\`, \`free-access\`
16045
- - **Billing Period** \u2014 one of: \`week\`, \`month\`, \`year\`
16046
- - **Period Length** \u2014 number of billing periods per cycle
16047
- - **Free Trial** \u2014 number of free trial days (optional)`
16048
- },
16049
- {
16050
- title: "Edit a Product",
16051
- path: "/docs/products-plans#edit-a-product",
16052
- content: `1. Go to **Products** in the left sidebar.
16053
- 2. Click on the product row you want to edit.
16054
- 3. Click **Edit** to modify name, description, status, or plans.`
16055
- },
16056
- {
16057
- title: "Checkout URL",
16058
- path: "/docs/products-plans#checkout-url",
16059
- content: `Each plan has a hosted checkout URL on the public checkout page at \`{checkout-base-url}/payment/{planId}\`. Sandbox and live are separate environments with separate databases and separate plan IDs \u2014 a sandbox plan does not exist on live, and \`go_live\` does not migrate products or plans (see the \`go_live\` tool description: "Does not copy products or checkout"). A sandbox checkout URL cannot be converted into a live checkout URL by editing the query string; the resulting link would target a plan that does not exist on live and would not take payments.
16060
-
16061
- Get a checkout URL from **create_product** or **list_products** \u2014 both return a ready-to-open \`checkoutUrl\` per plan (\`planCheckoutUrls[i].checkoutUrl\` on create, \`plans[i].checkoutUrl\` on list, or the top-level \`checkoutUrl\`). Use the returned URL verbatim \u2014 do not build one from \`planId\` yourself. These tools pick the environment themselves from the merchant's go-live status: sandbox before \`go_live\`, live once \`go_live\` has succeeded. There is nothing to switch on your side \u2014 same tools, same connection.
16062
-
16063
- - **Sandbox plan:** before go-live, \`checkoutUrl\` carries \`?isSandbox=true\` and points at the sandbox plan on the sandbox backend. Use it for test payments.
16064
- - **Live plan:** after \`go_live\` succeeds, those same tools resolve to the live backend for that merchant. Call **create_product** again with the product and plan details to create the plan on live; the \`checkoutUrl\` in that response has no query param and is the live link. **list_products** then returns live products only \u2014 the pre-go-live sandbox plan and its URL are not the live ones.
16065
-
16066
- Embed the returned URL directly, for example: \`<a href="<checkoutUrl from create_product or list_products>">Pay Now!</a>\`.`
16067
- }
16068
- ];
16069
-
16070
- // src/tools/docs/knowledge/checkout-customization.ts
16071
- var CHECKOUT_CUSTOMIZATION_DOC_SECTIONS = [
16072
- {
16073
- title: "Checkout Customization \u2014 Overview",
16074
- path: "/docs/checkout-customization#overview",
16075
- content: `Checkout customization controls the branding of a merchant's hosted checkout (the customer-facing payment page at /payment/{planId}). Configurable fields:
16076
- - **themeMode** \u2014 \`light\` or \`dark\`
16077
- - **backgroundColor**, **buttonColor**, **buttonTextColor**, **fontColor** \u2014 6-digit hex like \`#1A2B3C\`
16078
- - **fontFamily** \u2014 \`Inter\` or \`Roboto\`
16079
- - **inputStyle** \u2014 \`rounded\` or \`square\`
16080
- - **logo** \u2014 set with the \`set_checkout_logo\` MCP tool (presigned PUT flow)
16081
-
16082
- Read with the \`get_checkout_customization\` MCP tool; change theme/colors with \`update_checkout_customization\`; set the logo with \`set_checkout_logo\`.`
16083
- },
16084
- {
16085
- title: "Checkout Customization \u2014 Legal terms",
16086
- path: "/docs/checkout-customization#legal-terms",
16087
- content: `Checkout customization is **branding only** \u2014 theme, colors, logo, fonts, and input styles. Legal terms and conditions are **not** configurable.
16088
-
16089
- Under the Merchant of Record (MoR) model, the customer's contract of sale is with Payments AI, not the merchant. Merchants cannot upload or configure checkout terms & conditions through the dashboard or MCP tools.
16090
-
16091
- Live hosted checkout does not display merchant-configurable terms. Platform terms apply at merchant sign-up.`
16092
- },
16093
- {
16094
- title: "Checkout Customization \u2014 Updating branding via MCP",
16095
- path: "/docs/checkout-customization#update",
16096
- content: `The \`update_checkout_customization\` tool MERGES the fields you send: only the fields in the call are changed, and any field you omit keeps its current value. So you can send just \`{ "buttonColor": "#0A84FF" }\` to recolor the button without touching the rest of the branding.
16097
-
16098
- To remove or reset a field back to default, use the dashboard \u2014 clearing a field is not supported over MCP. Colors must be 6-digit hex (include the leading \`#\`). The tool returns the full customization after the merge, including the current \`logoUrl\` if a logo was set (via \`set_checkout_logo\` or the dashboard). The logo itself is not updated through this tool \u2014 use \`set_checkout_logo\`.`
16099
- },
16100
- {
16101
- title: "Checkout Customization \u2014 Logo",
16102
- path: "/docs/checkout-customization#logo",
16103
- content: `Set the checkout logo with the \`set_checkout_logo\` MCP tool (two-step, SSRF-safe \u2014 the tool never accepts an imageUrl to fetch):
16104
-
16105
- 1. Call \`set_checkout_logo\` with \`action: "mint"\`, \`merchantId\`, \`fileName\`, and \`contentType\` (\`image/png\`, \`image/jpeg\`, \`image/jpg\`, or \`image/webp\`). Returns \`{ uploadUrl, path, expiresAt }\`.
16106
- 2. HTTP **PUT** the image bytes to \`uploadUrl\` with the same \`Content-Type\` (max 5 MB). Bytes never pass through the MCP tool.
16107
- 3. Call \`set_checkout_logo\` with \`action: "confirm"\`, \`merchantId\`, and the exact \`path\` from step 1. Returns \`{ logoUrl }\`.
16108
-
16109
- Requires \`checkout:write\` scope. Replaces any existing checkout logo. Read the current logo via \`get_checkout_customization\` (\`logoUrl\`).`
16110
- }
16111
- ];
16112
-
16113
- // src/tools/docs/knowledge/merchant-create.ts
16114
- var MERCHANT_CREATE_DOC_SECTIONS = [
16115
- {
16116
- title: "Check for an existing merchant before creating one",
16117
- path: "/docs/merchant-create#existing-merchant",
16118
- content: `Call \`get_my_merchant\` before \`create_merchant_account\`. It takes no arguments and returns every merchant the current connection is authorized for as \`{ merchantId, name, deployment }\`. A new agent session cannot remember a merchant created in an earlier one, so calling \`create_merchant_account\` blind produces a second merchant with its own products and its own checkout links \u2014 and \`merchantId\` is the value \`create_product\`, \`list_products\` and the checkout tools all key on, so the two sets never merge. An empty \`merchants\` array is the normal "none yet" answer rather than an error: create one. When more than one comes back, ask which to use instead of picking.`
16119
- },
16120
- {
16121
- title: "Create merchant account",
16122
- path: "/docs/merchant-create#create-merchant-account",
16123
- content: `Call \`create_merchant_account\` with the required \`name\` and \`email\`. \`phone\` is optional at create time \u2014 the merchant supplies it later during KYC/enrollment in the dashboard, so agents do not need to ask for a phone number to obtain a \`merchantId\`. If a phone is passed anyway, it must be in E.164 format with country code (example: \`+14155552671\`). Returns a \`merchantId\` shared across live and sandbox (dual provision).`
16124
- },
16125
- {
16126
- title: "Sandbox create journey and go_live",
16127
- path: "/docs/merchant-create#sandbox-go-live",
16128
- content: `After \`create_merchant_account\` dual-provisions live + sandbox with a shared merchantId, create products and update checkout on **sandbox** until you call \`go_live\`. \`go_live\` promotes the merchant to live routing; it does **not** copy products or checkout. Distinct from \`get_merchant_activation_status\` (payment-provider onboarding status).`
16129
- }
16130
- ];
16131
-
16132
- // src/tools/docs/search-payments-ai-docs.tool.ts
16133
- var ALL_DOC_SECTIONS = [
16134
- ...DOC_SECTIONS,
16135
- ...CHECKOUT_CUSTOMIZATION_DOC_SECTIONS,
16136
- ...MERCHANT_CREATE_DOC_SECTIONS
16137
- ];
16138
- function resolveCheckoutPlaceholder(sections) {
16139
- const base = resolveCheckoutBaseUrl();
16140
- if (!base) {
16141
- return sections;
16142
- }
16143
- return sections.map((section) => ({
16144
- ...section,
16145
- content: section.content.replaceAll("{checkout-base-url}", base)
16146
- }));
16147
- }
16148
- function runSearchPaymentsAiDocsHandler(query) {
16149
- const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 1);
16150
- if (terms.length === 0) {
16151
- return { results: resolveCheckoutPlaceholder(ALL_DOC_SECTIONS) };
16152
- }
16153
- const scored = ALL_DOC_SECTIONS.map((section) => {
16154
- const haystack = `${section.title} ${section.content}`.toLowerCase();
16155
- const hits = terms.filter((t) => haystack.includes(t)).length;
16156
- return { section, hits };
16157
- });
16158
- const matches = scored.filter((s) => s.hits > 0).sort((a, b) => b.hits - a.hits);
16159
- return {
16160
- results: resolveCheckoutPlaceholder(
16161
- matches.length > 0 ? matches.map((s) => s.section) : ALL_DOC_SECTIONS
16162
- )
16163
- };
16164
- }
16165
-
16166
- // src/tools/health/payments-ai-health.tool.ts
16167
- async function buildPaymentsAiHealthOutput(backendClient) {
16168
- const base = {
16169
- serverName: "pai-managed-mcp-server",
16170
- version: readMcpServerVersion(),
16171
- backend: { baseUrlConfigured: false }
16172
- };
16173
- if (!backendClient) {
16174
- return paymentsAiHealthOutputSchema.parse(base);
16175
- }
16176
- const result = await backendClient.get(BACKEND_HTTP_HEALTH_PATH);
16177
- if (!result.ok) {
16178
- return paymentsAiHealthOutputSchema.parse({
16179
- ...base,
16180
- backend: {
16181
- baseUrlConfigured: true,
16182
- probe: {
16183
- path: "live backend",
16184
- ok: false,
16185
- message: result.message
16186
- }
16187
- }
16188
- });
16189
- }
16190
- let message = "Backend responded successfully.";
16191
- try {
16192
- const json2 = JSON.parse(result.bodyText);
16193
- if (typeof json2.message === "string" && json2.message.length > 0) {
16194
- message = json2.message;
16195
- }
16196
- } catch {
16197
- message = result.bodyText.slice(0, 500) || message;
16198
- }
16199
- return paymentsAiHealthOutputSchema.parse({
16200
- ...base,
16201
- backend: {
16202
- baseUrlConfigured: true,
16203
- probe: {
16204
- path: "live backend",
16205
- ok: true,
16206
- message
16207
- }
16208
- }
16209
- });
16210
- }
16211
-
16212
- // src/tools/merchant/create-merchant-account.tool.ts
16213
- import * as crypto from "node:crypto";
16214
- var randomUuidV7 = crypto.randomUUIDv7;
16215
- var STUB_MERCHANT = {
16216
- merchantId: "01930000-0000-7000-8000-000000000001"
16217
- };
16218
- function buildCreateMerchantAccountStub(_input) {
16219
- return createMerchantAccountOutputSchema.parse(STUB_MERCHANT);
16220
- }
16221
- function runCreateMerchantAccountHandler(raw) {
16222
- const parsed = createMerchantAccountInputSchema.safeParse(raw);
16223
- if (!parsed.success) {
16224
- return {
16225
- ok: false,
16226
- errorText: `Invalid arguments for create_merchant_account.
16227
- ${formatZodIssuesForTool(parsed.error.issues)}`
16228
- };
16229
- }
16230
- return { ok: true, data: buildCreateMerchantAccountStub(parsed.data) };
16231
- }
16232
- function formatDualProvisionPartialFailure(params) {
16233
- const livePart = params.live.ok ? "live: ok" : `live: ${params.live.errorText}`;
16234
- const sandboxPart = params.sandbox.ok ? "sandbox: ok" : `sandbox: ${params.sandbox.errorText}`;
16235
- return `Could not create your merchant account on both backends (partial failure).
16236
- merchantId: ${params.merchantId}
16237
- ${livePart}
16238
- ${sandboxPart}
16239
- Recovery: retry create_merchant_account with the same merchantId to finish provisioning without creating a new id.`;
16240
- }
16241
- function formatSandboxOnlyProvisionWarning(params) {
16242
- return `Warning: the sandbox copy of this merchant account was not created.
16243
- sandbox: ${params.sandboxErrorText}
16244
- The live merchant account exists and this merchantId is safe to use.
16245
- Sandbox products and sandbox checkout links stay unavailable until this succeeds.
16246
- Recovery: retry create_merchant_account with the same merchantId to finish sandbox provisioning without creating a new id.`;
16247
- }
16248
- function withMatchingId(side, merchantId) {
16249
- if (!side.ok || side.merchantId === merchantId) {
16250
- return side;
16251
- }
16252
- return { ok: false, errorText: UNEXPECTED_RESPONSE_ERROR };
16253
- }
16254
- function logDualProvisionOutcome(params) {
16255
- console.error("[create_merchant_account] dual-provision", {
16256
- merchantId: params.merchantId,
16257
- outcome: params.outcome,
16258
- live: params.live.ok ? "ok" : "error",
16259
- sandbox: params.sandbox.ok ? "ok" : "error",
16260
- liveError: params.live.ok ? void 0 : params.live.errorText,
16261
- sandboxError: params.sandbox.ok ? void 0 : params.sandbox.errorText
16262
- });
16263
- }
16264
- async function provisionOnBackend(backendClient, input, merchantId, identity) {
16265
- const result = await backendClient.post(BACKEND_CREATE_MERCHANT_ACCOUNT_PATH, {
16266
- name: input.name,
16267
- email: input.email,
16268
- ...input.phone ? { phone: input.phone } : {},
16269
- merchantId,
16270
- ...identity?.userId ? { userId: identity.userId } : {},
16271
- ...identity?.cognitoSub ? { cognitoSub: identity.cognitoSub } : {}
16272
- });
16273
- if (!result.ok) {
16274
- return {
16275
- ok: false,
16276
- errorText: formatToolBackendFailure("create your merchant account", result)
16277
- };
16278
- }
16279
- let parsedJson;
16280
- try {
16281
- parsedJson = JSON.parse(result.bodyText);
16282
- } catch {
16283
- return { ok: false, errorText: UNEXPECTED_RESPONSE_ERROR };
16284
- }
16285
- const out = createMerchantAccountOutputSchema.safeParse(parsedJson);
16286
- if (!out.success) {
16287
- return { ok: false, errorText: UNEXPECTED_RESPONSE_ERROR };
16288
- }
16289
- return { ok: true, merchantId: out.data.merchantId };
16290
- }
16291
- async function runCreateMerchantAccountHandlerWithDualBackend(clients, input, identity) {
16292
- const merchantId = input.merchantId ?? randomUuidV7();
16293
- const sharedOrigin = clients.live.getBaseUrl() === clients.sandbox.getBaseUrl();
16294
- if (sharedOrigin) {
16295
- const side = await provisionOnBackend(clients.live, input, merchantId, identity);
16296
- if (!side.ok) {
16297
- const failure = side;
16298
- logDualProvisionOutcome({
16299
- merchantId,
16300
- live: failure,
16301
- sandbox: failure,
16302
- outcome: "total_failure"
16303
- });
16304
- return {
16305
- ok: false,
16306
- errorText: formatDualProvisionPartialFailure({
16307
- merchantId,
16308
- live: failure,
16309
- sandbox: failure
16310
- })
16311
- };
16312
- }
16313
- const okSide = { ok: true, merchantId };
16314
- logDualProvisionOutcome({
16315
- merchantId,
16316
- live: okSide,
16317
- sandbox: okSide,
16318
- outcome: "success"
16319
- });
16320
- return { ok: true, data: { merchantId } };
16321
- }
16322
- const [liveSettled, sandboxSettled] = await Promise.allSettled([
16323
- provisionOnBackend(clients.live, input, merchantId, { userId: identity?.userId }),
16324
- provisionOnBackend(clients.sandbox, input, merchantId, { cognitoSub: identity?.cognitoSub })
16325
- ]);
16326
- const live = liveSettled.status === "fulfilled" ? liveSettled.value : {
16327
- ok: false,
16328
- errorText: liveSettled.reason instanceof Error ? liveSettled.reason.message : String(liveSettled.reason)
16329
- };
16330
- const sandbox = sandboxSettled.status === "fulfilled" ? sandboxSettled.value : {
16331
- ok: false,
16332
- errorText: sandboxSettled.reason instanceof Error ? sandboxSettled.reason.message : String(sandboxSettled.reason)
16333
- };
16334
- const liveSide = withMatchingId(live, merchantId);
16335
- const sandboxSide = withMatchingId(sandbox, merchantId);
16336
- if (liveSide.ok && sandboxSide.ok) {
16337
- logDualProvisionOutcome({
16338
- merchantId,
16339
- live: liveSide,
16340
- sandbox: sandboxSide,
16341
- outcome: "success"
16342
- });
16343
- return { ok: true, data: { merchantId } };
16344
- }
16345
- const outcome = !liveSide.ok && !sandboxSide.ok ? "total_failure" : "partial_failure";
16346
- logDualProvisionOutcome({ merchantId, live: liveSide, sandbox: sandboxSide, outcome });
16347
- if (liveSide.ok && !sandboxSide.ok) {
16348
- return {
16349
- ok: true,
16350
- data: { merchantId },
16351
- warningText: formatSandboxOnlyProvisionWarning({
16352
- sandboxErrorText: sandboxSide.errorText
16353
- })
16354
- };
16355
- }
16356
- return {
16357
- ok: false,
16358
- errorText: formatDualProvisionPartialFailure({
16359
- merchantId,
16360
- live: liveSide,
16361
- sandbox: sandboxSide
16362
- })
16363
- };
16364
- }
16365
-
16366
- // src/tools/merchant/get-my-merchant.tool.ts
16367
- var MERCHANT_LOOKUP_ACTION = "list the merchant accounts for this connection";
16368
- async function readMerchantName(live, merchantId) {
16369
- const result = await live.get(backendMerchantPath(merchantId));
16370
- if (!result.ok) {
16371
- return { ok: false, errorText: formatToolBackendFailure(MERCHANT_LOOKUP_ACTION, result) };
16372
- }
16373
- let parsedJson;
16374
- try {
16375
- parsedJson = JSON.parse(result.bodyText);
16376
- } catch {
16377
- return { ok: false, errorText: UNEXPECTED_RESPONSE_ERROR };
16463
+ return {
16464
+ ok: false,
16465
+ errorText: UNEXPECTED_RESPONSE_ERROR
16466
+ };
16378
16467
  }
16379
- const out = mcpMerchantSummarySchema.safeParse(parsedJson);
16468
+ const mapped = mapBackendListProductsResponse(parsedJson);
16469
+ const out = listProductsOutputSchema.safeParse(mapped);
16380
16470
  if (!out.success) {
16381
- return { ok: false, errorText: UNEXPECTED_RESPONSE_ERROR };
16471
+ return {
16472
+ ok: false,
16473
+ errorText: UNEXPECTED_RESPONSE_ERROR
16474
+ };
16382
16475
  }
16383
- return { ok: true, name: out.data.name };
16476
+ return { ok: true, data: out.data };
16384
16477
  }
16385
- async function runGetMyMerchantHandlerWithDualBackend(clients, merchantIds) {
16386
- if (merchantIds.length === 0) {
16387
- return { ok: true, data: getMyMerchantOutputSchema.parse({ merchants: [] }) };
16478
+
16479
+ // src/tools/docs/knowledge/products-plans.ts
16480
+ var DOC_SECTIONS = [
16481
+ {
16482
+ title: "Navigation \u2014 Products & Plans",
16483
+ path: "/docs/products-plans#navigation",
16484
+ content: `To access Products & Plans, find **Products** in the left sidebar under the "Product" group (Package icon). Click **Products** to navigate to /products.`
16485
+ },
16486
+ {
16487
+ title: "Create a Product",
16488
+ path: "/docs/products-plans#create-a-product",
16489
+ content: `1. Call **list_products** with merchantId to discover existing products and planIds (optional filters: status, q, page, limit).
16490
+ 2. Call **create_product** with merchantId, name, and at least one plan when you need a new product.
16491
+ 3. Optionally attach an image via the presigned upload flow: call **get_product_image_upload_url** with the returned product id, POST the raw bytes (multipart/form-data with the returned fields + file last) to the returned URL, then call **confirm_product_image** with the returned key.
16492
+ 4. Use the returned **checkoutUrl** (or **planCheckoutUrls[i].checkoutUrl**) directly \u2014 it already targets the right environment (sandbox URLs carry \`?isSandbox=true\`, live URLs have no query param). Do not construct the URL yourself.`
16493
+ },
16494
+ {
16495
+ title: "Upload a Product Image (MCP)",
16496
+ path: "/docs/products-plans#upload-product-image-mcp",
16497
+ content: `After create_product returns an id:
16498
+ 1. Call **get_product_image_upload_url** with merchantId, productId, fileName, and mimeType. It returns { url, fields, key, expiresIn }.
16499
+ 2. HTTP POST the raw file bytes to \`url\` as \`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). Example: \`curl -X POST -F "key=<fields.key>" -F "Content-Type=<mimeType>" -F "policy=<fields.policy>" -F "x-amz-signature=<fields['x-amz-signature']>" ... -F "file=@./logo.png" "<url>"\`.
16500
+ 3. Call **confirm_product_image** with merchantId, productId, and the returned key to validate and attach the image.
16501
+ 4. Supported formats: PNG, JPEG, WebP. Maximum size: 5 MB (enforced by S3 at upload time via a \`content-length-range\` policy condition \u2014 oversized bodies return a 403 from S3, before the backend ever sees them).
16502
+ 5. If confirm fails, retry get_product_image_upload_url + POST + confirm_product_image with the same productId \u2014 the product row is kept; the previous rejected upload was cleaned up by the backend.`
16503
+ },
16504
+ {
16505
+ title: "List Products (MCP)",
16506
+ path: "/docs/products-plans#list-products-mcp",
16507
+ content: `Call **list_products** with merchantId to list existing products and plans for a merchant. Returns paginated data with planId, name, amount, type, billing period, and checkoutUrl per plan. Optional filters: q (search by name), status (draft|active|archived), type (one-time|recurring|free-access), dateRangeStart/dateRangeEnd, page, limit. Use returned planId or checkoutUrl to build payment links \u2014 see Checkout URL docs. Targets sandbox until go_live.`
16508
+ },
16509
+ {
16510
+ title: "Plan Fields",
16511
+ path: "/docs/products-plans#plan-fields",
16512
+ content: `Each plan has the following fields:
16513
+ - **Name** \u2014 display name for the plan (max 30 characters)
16514
+ - **Amount** \u2014 price in USD as a decimal dollar amount (e.g. 9.99 = $9.99)
16515
+ - **Type** \u2014 one of: \`one-time\`, \`recurring\`, \`free-access\`
16516
+ - **Billing Period** \u2014 one of: \`week\`, \`month\`, \`year\`
16517
+ - **Period Length** \u2014 number of billing periods per cycle
16518
+ - **Free Trial** \u2014 number of free trial days (optional)`
16519
+ },
16520
+ {
16521
+ title: "Edit a Product",
16522
+ path: "/docs/products-plans#edit-a-product",
16523
+ content: `1. Go to **Products** in the left sidebar.
16524
+ 2. Click on the product row you want to edit.
16525
+ 3. Click **Edit** to modify name, description, status, or plans.`
16526
+ },
16527
+ {
16528
+ title: "Checkout URL",
16529
+ path: "/docs/products-plans#checkout-url",
16530
+ content: `Each plan has a hosted checkout URL on the public checkout page at \`{checkout-base-url}/payment/{planId}\`. Sandbox and live are separate environments with separate databases and separate plan IDs \u2014 a sandbox plan does not exist on live, and \`go_live\` does not migrate products or plans (see the \`go_live\` tool description: "Does not copy products or checkout"). A sandbox checkout URL cannot be converted into a live checkout URL by editing the query string; the resulting link would target a plan that does not exist on live and would not take payments.
16531
+
16532
+ Get a checkout URL from **create_product** or **list_products** \u2014 both return a ready-to-open \`checkoutUrl\` per plan (\`planCheckoutUrls[i].checkoutUrl\` on create, \`plans[i].checkoutUrl\` on list, or the top-level \`checkoutUrl\`). Use the returned URL verbatim \u2014 do not build one from \`planId\` yourself. These tools pick the environment themselves from the merchant's go-live status: sandbox before \`go_live\`, live once \`go_live\` has succeeded. There is nothing to switch on your side \u2014 same tools, same connection.
16533
+
16534
+ - **Sandbox plan:** before go-live, \`checkoutUrl\` carries \`?isSandbox=true\` and points at the sandbox plan on the sandbox backend. Use it for test payments.
16535
+ - **Live plan:** after \`go_live\` succeeds, those same tools resolve to the live backend for that merchant. Call **create_product** again with the product and plan details to create the plan on live; the \`checkoutUrl\` in that response has no query param and is the live link. **list_products** then returns live products only \u2014 the pre-go-live sandbox plan and its URL are not the live ones.
16536
+
16537
+ Embed the returned URL directly, for example: \`<a href="<checkoutUrl from create_product or list_products>">Pay Now!</a>\`.`
16388
16538
  }
16389
- const resolved = await Promise.all(
16390
- merchantIds.map(async (merchantId) => {
16391
- const [name, route] = await Promise.all([
16392
- readMerchantName(clients.live, merchantId),
16393
- resolveProductCheckoutBackend(clients, merchantId)
16394
- ]);
16395
- if (!name.ok) {
16396
- return { errorText: name.errorText };
16397
- }
16398
- return { merchantId, name: name.name, deployment: route.deployment };
16399
- })
16400
- );
16401
- const failure = resolved.find((entry) => "errorText" in entry);
16402
- if (failure) {
16403
- return { ok: false, errorText: failure.errorText };
16539
+ ];
16540
+
16541
+ // src/tools/docs/knowledge/checkout-customization.ts
16542
+ var CHECKOUT_CUSTOMIZATION_DOC_SECTIONS = [
16543
+ {
16544
+ title: "Checkout Customization \u2014 Overview",
16545
+ path: "/docs/checkout-customization#overview",
16546
+ content: `Checkout customization controls the branding of a merchant's hosted checkout (the customer-facing payment page at /payment/{planId}). Configurable fields:
16547
+ - **themeMode** \u2014 \`light\` or \`dark\`
16548
+ - **backgroundColor**, **buttonColor**, **buttonTextColor**, **fontColor** \u2014 6-digit hex like \`#1A2B3C\`
16549
+ - **fontFamily** \u2014 \`Inter\` or \`Roboto\`
16550
+ - **inputStyle** \u2014 \`rounded\` or \`square\`
16551
+ - **logo** \u2014 set with the \`set_checkout_logo\` MCP tool (presigned PUT flow)
16552
+
16553
+ Read with the \`get_checkout_customization\` MCP tool; change theme/colors with \`update_checkout_customization\`; set the logo with \`set_checkout_logo\`.`
16554
+ },
16555
+ {
16556
+ title: "Checkout Customization \u2014 Legal terms",
16557
+ path: "/docs/checkout-customization#legal-terms",
16558
+ content: `Checkout customization is **branding only** \u2014 theme, colors, logo, fonts, and input styles. Legal terms and conditions are **not** configurable.
16559
+
16560
+ Under the Merchant of Record (MoR) model, the customer's contract of sale is with Payments AI, not the merchant. Merchants cannot upload or configure checkout terms & conditions through the dashboard or MCP tools.
16561
+
16562
+ Live hosted checkout does not display merchant-configurable terms. Platform terms apply at merchant sign-up.`
16563
+ },
16564
+ {
16565
+ title: "Checkout Customization \u2014 Updating branding via MCP",
16566
+ path: "/docs/checkout-customization#update",
16567
+ content: `The \`update_checkout_customization\` tool MERGES the fields you send: only the fields in the call are changed, and any field you omit keeps its current value. So you can send just \`{ "buttonColor": "#0A84FF" }\` to recolor the button without touching the rest of the branding.
16568
+
16569
+ To remove or reset a field back to default, use the dashboard \u2014 clearing a field is not supported over MCP. Colors must be 6-digit hex (include the leading \`#\`). The tool returns the full customization after the merge, including the current \`logoUrl\` if a logo was set (via \`set_checkout_logo\` or the dashboard). The logo itself is not updated through this tool \u2014 use \`set_checkout_logo\`.`
16570
+ },
16571
+ {
16572
+ title: "Checkout Customization \u2014 Logo",
16573
+ path: "/docs/checkout-customization#logo",
16574
+ content: `Set the checkout logo with the \`set_checkout_logo\` MCP tool (two-step, SSRF-safe \u2014 the tool never accepts an imageUrl to fetch):
16575
+
16576
+ 1. Call \`set_checkout_logo\` with \`action: "mint"\`, \`merchantId\`, \`fileName\`, and \`contentType\` (\`image/png\`, \`image/jpeg\`, \`image/jpg\`, or \`image/webp\`). Returns \`{ uploadUrl, path, expiresAt }\`.
16577
+ 2. HTTP **PUT** the image bytes to \`uploadUrl\` with the same \`Content-Type\` (max 5 MB). Bytes never pass through the MCP tool.
16578
+ 3. Call \`set_checkout_logo\` with \`action: "confirm"\`, \`merchantId\`, and the exact \`path\` from step 1. Returns \`{ logoUrl }\`.
16579
+
16580
+ Requires \`checkout:write\` scope. Replaces any existing checkout logo. Read the current logo via \`get_checkout_customization\` (\`logoUrl\`).`
16581
+ }
16582
+ ];
16583
+
16584
+ // src/tools/docs/knowledge/merchant-create.ts
16585
+ var MERCHANT_CREATE_DOC_SECTIONS = [
16586
+ {
16587
+ title: "Check for an existing merchant before creating one",
16588
+ path: "/docs/merchant-create#existing-merchant",
16589
+ content: `Call \`get_my_merchant\` before \`create_merchant_account\`. It takes no arguments and returns every merchant the current connection is authorized for as \`{ merchantId, name, deployment }\`. A new agent session cannot remember a merchant created in an earlier one, so calling \`create_merchant_account\` blind produces a second merchant with its own products and its own checkout links \u2014 and \`merchantId\` is the value \`create_product\`, \`list_products\` and the checkout tools all key on, so the two sets never merge. An empty \`merchants\` array is the normal "none yet" answer rather than an error: create one. When more than one comes back, ask which to use instead of picking.`
16590
+ },
16591
+ {
16592
+ title: "Create merchant account",
16593
+ path: "/docs/merchant-create#create-merchant-account",
16594
+ content: `Call \`create_merchant_account\` with the required \`name\` and \`email\`. The \`name\` must be unique across Payments AI \u2014 pick a distinct business name for each new merchant (append a suffix or qualifier rather than reusing "Quickstart Demo"). \`phone\` is optional at create time \u2014 the merchant supplies it later during KYC/enrollment in the dashboard, so agents do not need to ask for a phone number to obtain a \`merchantId\`. If a phone is passed anyway, it must be in E.164 format with country code (example: \`+14155552671\`). Returns a \`merchantId\` shared across live and sandbox (dual provision). If both backends fail with \`business_name_taken\`, retry with a different \`name\` (do not keep retrying the same name). If only one backend failed, retry with the same \`merchantId\`, \`name\`, and \`email\` to finish provisioning.`
16595
+ },
16596
+ {
16597
+ title: "Sandbox create journey and go_live",
16598
+ path: "/docs/merchant-create#sandbox-go-live",
16599
+ content: `After \`create_merchant_account\` dual-provisions live + sandbox with a shared merchantId, create products and update checkout on **sandbox** until you call \`go_live\`. \`go_live\` promotes the merchant to live routing; it does **not** copy products or checkout. Distinct from \`get_merchant_activation_status\` (payment-provider onboarding status).`
16600
+ }
16601
+ ];
16602
+
16603
+ // src/tools/docs/search-payments-ai-docs.tool.ts
16604
+ var ALL_DOC_SECTIONS = [
16605
+ ...DOC_SECTIONS,
16606
+ ...CHECKOUT_CUSTOMIZATION_DOC_SECTIONS,
16607
+ ...MERCHANT_CREATE_DOC_SECTIONS
16608
+ ];
16609
+ function resolveCheckoutPlaceholder(sections) {
16610
+ const base = resolveCheckoutBaseUrl();
16611
+ if (!base) {
16612
+ return sections;
16613
+ }
16614
+ return sections.map((section) => ({
16615
+ ...section,
16616
+ content: section.content.replaceAll("{checkout-base-url}", base)
16617
+ }));
16618
+ }
16619
+ function runSearchPaymentsAiDocsHandler(query) {
16620
+ const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 1);
16621
+ if (terms.length === 0) {
16622
+ return { results: resolveCheckoutPlaceholder(ALL_DOC_SECTIONS) };
16404
16623
  }
16624
+ const scored = ALL_DOC_SECTIONS.map((section) => {
16625
+ const haystack = `${section.title} ${section.content}`.toLowerCase();
16626
+ const hits = terms.filter((t) => haystack.includes(t)).length;
16627
+ return { section, hits };
16628
+ });
16629
+ const matches = scored.filter((s) => s.hits > 0).sort((a, b) => b.hits - a.hits);
16405
16630
  return {
16406
- ok: true,
16407
- data: getMyMerchantOutputSchema.parse({ merchants: resolved })
16631
+ results: resolveCheckoutPlaceholder(
16632
+ matches.length > 0 ? matches.map((s) => s.section) : ALL_DOC_SECTIONS
16633
+ )
16408
16634
  };
16409
16635
  }
16410
16636
 
@@ -16585,13 +16811,6 @@ async function runSetCheckoutLogoHandler(backendClient, input) {
16585
16811
 
16586
16812
  // src/tools/tool-annotations.ts
16587
16813
  var TOOL_ANNOTATIONS = {
16588
- [MCP_TOOL_NAMES.PAYMENTS_AI_HEALTH]: {
16589
- title: "Payments AI Health",
16590
- readOnlyHint: true,
16591
- destructiveHint: false,
16592
- idempotentHint: true,
16593
- openWorldHint: false
16594
- },
16595
16814
  [MCP_TOOL_NAMES.GET_MERCHANT_ACTIVATION_STATUS]: {
16596
16815
  title: "Get Merchant Activation Status",
16597
16816
  readOnlyHint: true,
@@ -16681,11 +16900,10 @@ var TOOL_ANNOTATIONS = {
16681
16900
  // src/tools/tool-descriptions.ts
16682
16901
  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; there is no long-copy field that reaches buyers today, so overflow must be edited down rather than moved.`;
16683
16902
  var TOOL_DESCRIPTIONS = {
16684
- [MCP_TOOL_NAMES.PAYMENTS_AI_HEALTH]: "Check whether the PAI Managed MCP server is healthy. Returns server version and connectivity status.",
16685
16903
  [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.',
16686
- [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. Requires only name and email. Phone is optional and, if provided, must be in E.164 format with country code (e.g. +14155552671); otherwise it is collected later during KYC/enrollment in the dashboard. Dual-provisions live and sandbox with a shared merchantId. Returns that merchant ID. Next: create_product (and checkout tools) on sandbox, then go_live when ready.",
16904
+ [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. Requires only name and email. The business name must be unique across Payments AI \u2014 do not reuse a name from another merchant (add a suffix or qualifier if needed). Phone is optional and, if provided, must be in E.164 format with country code (e.g. +14155552671); otherwise it is collected later during KYC/enrollment in the dashboard. Dual-provisions live and sandbox with a shared merchantId. On partial dual-provision failure, retry with the same merchantId, name, and email. If both backends fail with business_name_taken, change the name before retrying. Returns that merchant ID. Next: create_product (and checkout tools) on sandbox, then go_live when ready.",
16687
16905
  [MCP_TOOL_NAMES.GET_MERCHANT_ACTIVATION_STATUS]: 'Get the production activation status of a merchant account on PAI Managed. Returns one of "active" (the live merchant is linked to a payment-provider account \u2014 proceed with live-key operations), "not_provisioned" (no live merchant record exists \u2014 dual provision never finished on live; recovery: re-run create_merchant_account with the same merchantId, which reuses that id idempotently \u2014 go_live cannot fix this and fails while the live twin is missing), or "pending" (a live merchant record exists but has no payment-provider link \u2014 it does not clear on its own, and re-running create_merchant_account with the same merchantId returns the existing record without provisioning, so escalate to support instead of polling).',
16688
- [MCP_TOOL_NAMES.GO_LIVE]: "Promote a sandbox merchant to live by setting liveMerchantId (Merchant Go-Live). 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. Does not copy products or checkout. Distinct from get_merchant_activation_status (payment-provider onboarding status) and from ApiKeyEnvironment.",
16906
+ [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. Does not copy products or checkout. Distinct from get_merchant_activation_status (payment-provider onboarding status) and from ApiKeyEnvironment.",
16689
16907
  [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.",
16690
16908
  [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 (in USD as a decimal dollar amount, e.g. 9.99 for $9.99 \u2014 NOT cents), billing type (one-time, recurring, or free-access), and billing period. " + 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. 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.",
16691
16909
  [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.',
@@ -16695,6 +16913,20 @@ var TOOL_DESCRIPTIONS = {
16695
16913
  [MCP_TOOL_NAMES.UPDATE_CHECKOUT_CUSTOMIZATION]: "Update a merchant's checkout customization on PAI Managed. Fields are MERGED: send only the fields you want to change \u2014 any field you omit is left unchanged. To remove/reset a field, use the dashboard (clearing is not supported over MCP). Colors are 6-digit hex like '#1A2B3C'; fontFamily is 'Inter' or 'Roboto'; themeMode is 'light' or 'dark'; inputStyle is 'rounded' or 'square'. The logo is set with set_checkout_logo (mint \u2192 PUT \u2192 confirm), not this tool. Returns the full customization after the merge.",
16696
16914
  [MCP_TOOL_NAMES.SET_CHECKOUT_LOGO]: 'Set a merchant checkout logo via a two-step presigned PUT flow (image bytes never pass through the tool). Step 1 \u2014 action "mint" with merchantId, fileName, and contentType (image/png, image/jpeg, image/jpg, or image/webp): returns { uploadUrl, path, expiresAt }. HTTP PUT the file bytes to uploadUrl with the same Content-Type (\u2264 5 MB). Step 2 \u2014 action "confirm" with merchantId and the exact path from mint: returns { logoUrl }. Does not accept an imageUrl to fetch server-side (SSRF-safe). Replaces any existing checkout logo.'
16697
16915
  };
16916
+ var SOLO_TOOL_DESCRIPTION_OVERRIDES = {
16917
+ [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 }] }. On this sandbox MCP URL, "deployment" is always "sandbox". 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.',
16918
+ [MCP_TOOL_NAMES.CREATE_MERCHANT_ACCOUNT]: "Create a merchant account on Payments AI sandbox. 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. Requires only name and email. The business name must be unique across Payments AI \u2014 do not reuse a name from another merchant (add a suffix or qualifier if needed). Phone is optional and, if provided, must be in E.164 format with country code (e.g. +14155552671); otherwise it is collected later during KYC/enrollment in the dashboard. Creates the merchant on sandbox only. On failure, retry with the same merchantId, name, and email. If create fails with business_name_taken, change the name before retrying. Returns that merchant ID. Next: create_product (and checkout tools) on this sandbox MCP URL. Promoting a merchant to live is not available here \u2014 connect to the live MCP URL when you are ready.",
16919
+ [MCP_TOOL_NAMES.GET_MERCHANT_ACTIVATION_STATUS]: 'Get the payment-provider activation status of a merchant account on this sandbox MCP URL. Returns one of "active" (the merchant is linked to a payment-provider account), "not_provisioned" (no merchant record exists on sandbox; recovery: re-run create_merchant_account with the same merchantId, which reuses that id idempotently), or "pending" (a merchant record exists but has no payment-provider link \u2014 it does not clear on its own, and re-running create_merchant_account with the same merchantId returns the existing record without provisioning, so escalate to support instead of polling).',
16920
+ [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.",
16921
+ [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 (in USD as a decimal dollar amount, e.g. 9.99 for $9.99 \u2014 NOT cents), billing type (one-time, recurring, or free-access), and billing period. " + 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`). 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.",
16922
+ [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, type, billing period) 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."
16923
+ };
16924
+ function resolveToolDescriptions(mode = "dual") {
16925
+ if (mode !== "solo") {
16926
+ return TOOL_DESCRIPTIONS;
16927
+ }
16928
+ return { ...TOOL_DESCRIPTIONS, ...SOLO_TOOL_DESCRIPTION_OVERRIDES };
16929
+ }
16698
16930
 
16699
16931
  // src/observability/log-mcp-tool-call.ts
16700
16932
  import { createHash } from "node:crypto";
@@ -16740,17 +16972,17 @@ function toolError(text) {
16740
16972
  }
16741
16973
  function resolveBackendClients(options2) {
16742
16974
  if (options2?.backendClients) {
16743
- return options2.backendClients;
16975
+ return {
16976
+ sandbox: options2.backendClients.sandbox,
16977
+ live: options2.backendClients.live
16978
+ };
16744
16979
  }
16745
16980
  const fromEnv = createDualBackendClientsFromEnv();
16746
16981
  if (fromEnv) {
16747
- return fromEnv;
16982
+ return { sandbox: fromEnv.sandbox, live: fromEnv.live };
16748
16983
  }
16749
16984
  return { sandbox: void 0, live: void 0 };
16750
16985
  }
16751
- function isMerchantAllowed(merchantId, payload) {
16752
- return !payload || payload.merchantIds.includes(merchantId);
16753
- }
16754
16986
  var BACKEND_UNREACHABLE_ERROR = "The PAI Managed backend is not reachable. Contact Payments AI support.";
16755
16987
  var CHECKOUT_URL_NOT_CONFIGURED_ERROR = "The hosted checkout URL is not configured on the PAI Managed MCP server (set CHECKOUT_URL). Contact Payments AI support.";
16756
16988
  async function resolveProductCheckoutRoute(sandbox, live, merchantId) {
@@ -16761,32 +16993,39 @@ async function resolveProductCheckoutRoute(sandbox, live, merchantId) {
16761
16993
  }
16762
16994
  function registerTools(server, options2) {
16763
16995
  const { sandbox: sandboxBackendClient, live: liveBackendClient } = resolveBackendClients(options2);
16996
+ const dualBackendClients = sandboxBackendClient ? { sandbox: sandboxBackendClient, live: liveBackendClient } : void 0;
16997
+ const deploymentRouter2 = options2?.deploymentRouter ?? (dualBackendClients && hasLiveBackend(dualBackendClients) ? createDualRouter(dualBackendClients) : void 0);
16764
16998
  const mcpTokenPayload = options2?.mcpTokenPayload;
16765
16999
  const checkoutBaseUrl2 = options2?.checkoutBaseUrl ?? resolveCheckoutBaseUrl();
16766
17000
  const dashboardUrl = options2?.dashboardUrl;
17001
+ const toolDescriptions = resolveToolDescriptions(deploymentRouter2?.mode ?? "dual");
16767
17002
  const denyForScope = (scope) => scopeDenial(scope, mcpTokenPayload, dashboardUrl);
16768
- server.registerTool(
16769
- MCP_TOOL_NAMES.PAYMENTS_AI_HEALTH,
16770
- {
16771
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.PAYMENTS_AI_HEALTH],
16772
- annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.PAYMENTS_AI_HEALTH],
16773
- outputSchema: paymentsAiHealthOutputSchema
16774
- },
16775
- async () => {
16776
- const denial = denyForScope("health:read");
16777
- if (denial) return denial;
16778
- logMcpToolCallAfterAuth(MCP_TOOL_NAMES.PAYMENTS_AI_HEALTH, {}, mcpTokenPayload?.userId);
16779
- const structuredContent = await buildPaymentsAiHealthOutput(liveBackendClient);
16780
- return {
16781
- content: [{ type: "text", text: JSON.stringify(structuredContent) }],
16782
- structuredContent
16783
- };
17003
+ async function merchantAuthorizationDenial(merchantId) {
17004
+ if (!mcpTokenPayload) {
17005
+ return void 0;
16784
17006
  }
16785
- );
17007
+ if (!deploymentRouter2) {
17008
+ return mcpTokenPayload.merchantIds.includes(merchantId) ? void 0 : toolError("Merchant not authorized for this MCP key.");
17009
+ }
17010
+ const allowlist = await deploymentRouter2.resolveMerchantAllowlist(mcpTokenPayload);
17011
+ if (allowlist && typeof allowlist === "object" && "error" in allowlist) {
17012
+ return toolError(allowlist.error);
17013
+ }
17014
+ if (allowlist?.includes(merchantId)) {
17015
+ return void 0;
17016
+ }
17017
+ return toolError("Merchant not authorized for this MCP key.");
17018
+ }
17019
+ async function resolveProductCheckoutRouteForTool(merchantId) {
17020
+ if (deploymentRouter2 && dualBackendClients) {
17021
+ return deploymentRouter2.resolveProductCheckout(dualBackendClients, merchantId);
17022
+ }
17023
+ return resolveProductCheckoutRoute(sandboxBackendClient, liveBackendClient, merchantId);
17024
+ }
16786
17025
  server.registerTool(
16787
17026
  MCP_TOOL_NAMES.GET_MY_MERCHANT,
16788
17027
  {
16789
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.GET_MY_MERCHANT],
17028
+ description: toolDescriptions[MCP_TOOL_NAMES.GET_MY_MERCHANT],
16790
17029
  annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.GET_MY_MERCHANT],
16791
17030
  outputSchema: getMyMerchantOutputSchema
16792
17031
  },
@@ -16794,13 +17033,18 @@ function registerTools(server, options2) {
16794
17033
  const denial = denyForScope("merchant:read");
16795
17034
  if (denial) return denial;
16796
17035
  logMcpToolCallAfterAuth(MCP_TOOL_NAMES.GET_MY_MERCHANT, {}, mcpTokenPayload?.userId);
16797
- if (!sandboxBackendClient || !liveBackendClient) {
17036
+ if (!dualBackendClients || !deploymentRouter2) {
16798
17037
  return toolError(BACKEND_UNREACHABLE_ERROR);
16799
17038
  }
16800
- const result = await runGetMyMerchantHandlerWithDualBackend(
16801
- { sandbox: sandboxBackendClient, live: liveBackendClient },
16802
- mcpTokenPayload?.merchantIds ?? []
16803
- );
17039
+ let merchantIds = [];
17040
+ if (mcpTokenPayload !== void 0) {
17041
+ const allowlist = await deploymentRouter2.resolveMerchantAllowlist(mcpTokenPayload);
17042
+ if (allowlist && typeof allowlist === "object" && "error" in allowlist) {
17043
+ return toolError(allowlist.error);
17044
+ }
17045
+ merchantIds = allowlist ?? [];
17046
+ }
17047
+ const result = await deploymentRouter2.getMyMerchant(dualBackendClients, merchantIds);
16804
17048
  if (!result.ok) {
16805
17049
  return toolError(result.errorText);
16806
17050
  }
@@ -16813,7 +17057,7 @@ function registerTools(server, options2) {
16813
17057
  server.registerTool(
16814
17058
  MCP_TOOL_NAMES.CREATE_MERCHANT_ACCOUNT,
16815
17059
  {
16816
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.CREATE_MERCHANT_ACCOUNT],
17060
+ description: toolDescriptions[MCP_TOOL_NAMES.CREATE_MERCHANT_ACCOUNT],
16817
17061
  annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.CREATE_MERCHANT_ACCOUNT],
16818
17062
  inputSchema: createMerchantAccountInputSchema,
16819
17063
  outputSchema: createMerchantAccountOutputSchema
@@ -16826,14 +17070,10 @@ function registerTools(server, options2) {
16826
17070
  args,
16827
17071
  mcpTokenPayload?.userId
16828
17072
  );
16829
- const result = liveBackendClient && sandboxBackendClient ? await runCreateMerchantAccountHandlerWithDualBackend(
16830
- { live: liveBackendClient, sandbox: sandboxBackendClient },
16831
- args,
16832
- {
16833
- userId: mcpTokenPayload?.userId,
16834
- cognitoSub: mcpTokenPayload?.cognitoSub
16835
- }
16836
- ) : runCreateMerchantAccountHandler(args);
17073
+ const result = deploymentRouter2 && dualBackendClients ? await deploymentRouter2.createMerchantAccount(dualBackendClients, args, {
17074
+ userId: mcpTokenPayload?.userId,
17075
+ cognitoSub: mcpTokenPayload?.cognitoSub
17076
+ }) : runCreateMerchantAccountHandler(args);
16837
17077
  if (!result.ok) {
16838
17078
  return toolError(result.errorText);
16839
17079
  }
@@ -16849,7 +17089,7 @@ function registerTools(server, options2) {
16849
17089
  server.registerTool(
16850
17090
  MCP_TOOL_NAMES.GET_MERCHANT_ACTIVATION_STATUS,
16851
17091
  {
16852
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.GET_MERCHANT_ACTIVATION_STATUS],
17092
+ description: toolDescriptions[MCP_TOOL_NAMES.GET_MERCHANT_ACTIVATION_STATUS],
16853
17093
  annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.GET_MERCHANT_ACTIVATION_STATUS],
16854
17094
  inputSchema: activationStatusInputSchema,
16855
17095
  outputSchema: activationStatusOutputSchema
@@ -16857,15 +17097,16 @@ function registerTools(server, options2) {
16857
17097
  async (args) => {
16858
17098
  const denial = denyForScope("merchant:read");
16859
17099
  if (denial) return denial;
16860
- if (!isMerchantAllowed(args.merchantId, mcpTokenPayload)) {
16861
- return toolError("Merchant not authorized for this MCP key.");
17100
+ const activationDenial = await merchantAuthorizationDenial(args.merchantId);
17101
+ if (activationDenial) {
17102
+ return activationDenial;
16862
17103
  }
16863
17104
  logMcpToolCallAfterAuth(
16864
17105
  MCP_TOOL_NAMES.GET_MERCHANT_ACTIVATION_STATUS,
16865
17106
  args,
16866
17107
  mcpTokenPayload?.userId
16867
17108
  );
16868
- const result = liveBackendClient ? await runActivationStatusHandlerWithBackend(liveBackendClient, args) : runActivationStatusHandler(args);
17109
+ const result = deploymentRouter2 && dualBackendClients ? await deploymentRouter2.activationStatus(dualBackendClients, args) : liveBackendClient ? await runActivationStatusHandlerWithBackend(liveBackendClient, args) : runActivationStatusHandler(args);
16869
17110
  if (!result.ok) {
16870
17111
  return toolError(result.errorText);
16871
17112
  }
@@ -16875,39 +17116,38 @@ function registerTools(server, options2) {
16875
17116
  };
16876
17117
  }
16877
17118
  );
16878
- server.registerTool(
16879
- MCP_TOOL_NAMES.GO_LIVE,
16880
- {
16881
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.GO_LIVE],
16882
- annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.GO_LIVE],
16883
- inputSchema: goLiveInputSchema,
16884
- outputSchema: goLiveOutputSchema
16885
- },
16886
- async (args) => {
16887
- const denial = denyForScope("merchant:go_live");
16888
- if (denial) return denial;
16889
- if (!isMerchantAllowed(args.merchantId, mcpTokenPayload)) {
16890
- return toolError("Merchant not authorized for this MCP key.");
16891
- }
16892
- logMcpToolCallAfterAuth(MCP_TOOL_NAMES.GO_LIVE, args, mcpTokenPayload?.userId);
16893
- const result = liveBackendClient && sandboxBackendClient ? await runGoLiveHandlerWithDualBackend(
16894
- { live: liveBackendClient, sandbox: sandboxBackendClient },
16895
- args,
16896
- mcpTokenPayload?.userId
16897
- ) : runGoLiveHandler(args);
16898
- if (!result.ok) {
16899
- return toolError(result.errorText);
17119
+ if (deploymentRouter2 === void 0 || deploymentRouter2.supportsGoLive) {
17120
+ server.registerTool(
17121
+ MCP_TOOL_NAMES.GO_LIVE,
17122
+ {
17123
+ description: toolDescriptions[MCP_TOOL_NAMES.GO_LIVE],
17124
+ annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.GO_LIVE],
17125
+ inputSchema: goLiveInputSchema,
17126
+ outputSchema: goLiveOutputSchema
17127
+ },
17128
+ async (args) => {
17129
+ const denial = denyForScope("merchant:go_live");
17130
+ if (denial) return denial;
17131
+ const goLiveMerchantDenial = await merchantAuthorizationDenial(args.merchantId);
17132
+ if (goLiveMerchantDenial) {
17133
+ return goLiveMerchantDenial;
17134
+ }
17135
+ logMcpToolCallAfterAuth(MCP_TOOL_NAMES.GO_LIVE, args, mcpTokenPayload?.userId);
17136
+ const result = deploymentRouter2 && dualBackendClients ? await deploymentRouter2.goLive(dualBackendClients, args, mcpTokenPayload?.userId) : runGoLiveHandler(args);
17137
+ if (!result.ok) {
17138
+ return toolError(result.errorText);
17139
+ }
17140
+ return {
17141
+ content: [{ type: "text", text: JSON.stringify(result.data) }],
17142
+ structuredContent: result.data
17143
+ };
16900
17144
  }
16901
- return {
16902
- content: [{ type: "text", text: JSON.stringify(result.data) }],
16903
- structuredContent: result.data
16904
- };
16905
- }
16906
- );
17145
+ );
17146
+ }
16907
17147
  server.registerTool(
16908
17148
  MCP_TOOL_NAMES.SEARCH_PAYMENTS_AI_DOCS,
16909
17149
  {
16910
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.SEARCH_PAYMENTS_AI_DOCS],
17150
+ description: toolDescriptions[MCP_TOOL_NAMES.SEARCH_PAYMENTS_AI_DOCS],
16911
17151
  annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.SEARCH_PAYMENTS_AI_DOCS],
16912
17152
  inputSchema: searchPaymentsAiDocsInputSchema,
16913
17153
  outputSchema: searchPaymentsAiDocsOutputSchema
@@ -16930,7 +17170,7 @@ function registerTools(server, options2) {
16930
17170
  server.registerTool(
16931
17171
  MCP_TOOL_NAMES.CREATE_PRODUCT,
16932
17172
  {
16933
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.CREATE_PRODUCT],
17173
+ description: toolDescriptions[MCP_TOOL_NAMES.CREATE_PRODUCT],
16934
17174
  annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.CREATE_PRODUCT],
16935
17175
  inputSchema: createMcpProductInputSchema,
16936
17176
  outputSchema: createMcpProductOutputSchema
@@ -16942,19 +17182,16 @@ function registerTools(server, options2) {
16942
17182
  if (!validation.ok) {
16943
17183
  return toolError(validation.errorText);
16944
17184
  }
16945
- if (!isMerchantAllowed(validation.data.merchantId, mcpTokenPayload)) {
16946
- return toolError("Merchant not authorized for this MCP key.");
17185
+ const createProductDenial = await merchantAuthorizationDenial(validation.data.merchantId);
17186
+ if (createProductDenial) {
17187
+ return createProductDenial;
16947
17188
  }
16948
17189
  logMcpToolCallAfterAuth(
16949
17190
  MCP_TOOL_NAMES.CREATE_PRODUCT,
16950
17191
  validation.data,
16951
17192
  mcpTokenPayload?.userId
16952
17193
  );
16953
- const route = await resolveProductCheckoutRoute(
16954
- sandboxBackendClient,
16955
- liveBackendClient,
16956
- validation.data.merchantId
16957
- );
17194
+ const route = await resolveProductCheckoutRouteForTool(validation.data.merchantId);
16958
17195
  if ("error" in route) {
16959
17196
  return toolError(route.error);
16960
17197
  }
@@ -16978,7 +17215,7 @@ function registerTools(server, options2) {
16978
17215
  server.registerTool(
16979
17216
  MCP_TOOL_NAMES.GET_PRODUCT_IMAGE_UPLOAD_URL,
16980
17217
  {
16981
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.GET_PRODUCT_IMAGE_UPLOAD_URL],
17218
+ description: toolDescriptions[MCP_TOOL_NAMES.GET_PRODUCT_IMAGE_UPLOAD_URL],
16982
17219
  annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.GET_PRODUCT_IMAGE_UPLOAD_URL],
16983
17220
  inputSchema: productImageUploadUrlInputSchema,
16984
17221
  outputSchema: productImageUploadUrlOutputSchema
@@ -16990,19 +17227,16 @@ function registerTools(server, options2) {
16990
17227
  if (!validation.ok) {
16991
17228
  return toolError(validation.errorText);
16992
17229
  }
16993
- if (!isMerchantAllowed(validation.data.merchantId, mcpTokenPayload)) {
16994
- return toolError("Merchant not authorized for this MCP key.");
17230
+ const uploadUrlDenial = await merchantAuthorizationDenial(validation.data.merchantId);
17231
+ if (uploadUrlDenial) {
17232
+ return uploadUrlDenial;
16995
17233
  }
16996
17234
  logMcpToolCallAfterAuth(
16997
17235
  MCP_TOOL_NAMES.GET_PRODUCT_IMAGE_UPLOAD_URL,
16998
17236
  validation.data,
16999
17237
  mcpTokenPayload?.userId
17000
17238
  );
17001
- const route = await resolveProductCheckoutRoute(
17002
- sandboxBackendClient,
17003
- liveBackendClient,
17004
- validation.data.merchantId
17005
- );
17239
+ const route = await resolveProductCheckoutRouteForTool(validation.data.merchantId);
17006
17240
  if ("error" in route) {
17007
17241
  return toolError(route.error);
17008
17242
  }
@@ -17022,7 +17256,7 @@ function registerTools(server, options2) {
17022
17256
  server.registerTool(
17023
17257
  MCP_TOOL_NAMES.CONFIRM_PRODUCT_IMAGE,
17024
17258
  {
17025
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.CONFIRM_PRODUCT_IMAGE],
17259
+ description: toolDescriptions[MCP_TOOL_NAMES.CONFIRM_PRODUCT_IMAGE],
17026
17260
  annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.CONFIRM_PRODUCT_IMAGE],
17027
17261
  inputSchema: confirmProductImageInputSchema,
17028
17262
  outputSchema: confirmProductImageOutputSchema
@@ -17034,19 +17268,16 @@ function registerTools(server, options2) {
17034
17268
  if (!validation.ok) {
17035
17269
  return toolError(validation.errorText);
17036
17270
  }
17037
- if (!isMerchantAllowed(validation.data.merchantId, mcpTokenPayload)) {
17038
- return toolError("Merchant not authorized for this MCP key.");
17271
+ const confirmImageDenial = await merchantAuthorizationDenial(validation.data.merchantId);
17272
+ if (confirmImageDenial) {
17273
+ return confirmImageDenial;
17039
17274
  }
17040
17275
  logMcpToolCallAfterAuth(
17041
17276
  MCP_TOOL_NAMES.CONFIRM_PRODUCT_IMAGE,
17042
17277
  validation.data,
17043
17278
  mcpTokenPayload?.userId
17044
17279
  );
17045
- const route = await resolveProductCheckoutRoute(
17046
- sandboxBackendClient,
17047
- liveBackendClient,
17048
- validation.data.merchantId
17049
- );
17280
+ const route = await resolveProductCheckoutRouteForTool(validation.data.merchantId);
17050
17281
  if ("error" in route) {
17051
17282
  return toolError(route.error);
17052
17283
  }
@@ -17066,7 +17297,7 @@ function registerTools(server, options2) {
17066
17297
  server.registerTool(
17067
17298
  MCP_TOOL_NAMES.LIST_PRODUCTS,
17068
17299
  {
17069
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.LIST_PRODUCTS],
17300
+ description: toolDescriptions[MCP_TOOL_NAMES.LIST_PRODUCTS],
17070
17301
  annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.LIST_PRODUCTS],
17071
17302
  inputSchema: listProductsInputSchema,
17072
17303
  outputSchema: listProductsOutputSchema
@@ -17078,19 +17309,16 @@ function registerTools(server, options2) {
17078
17309
  if (!validation.ok) {
17079
17310
  return toolError(validation.errorText);
17080
17311
  }
17081
- if (!isMerchantAllowed(validation.data.merchantId, mcpTokenPayload)) {
17082
- return toolError("Merchant not authorized for this MCP key.");
17312
+ const listProductsDenial = await merchantAuthorizationDenial(validation.data.merchantId);
17313
+ if (listProductsDenial) {
17314
+ return listProductsDenial;
17083
17315
  }
17084
17316
  logMcpToolCallAfterAuth(
17085
17317
  MCP_TOOL_NAMES.LIST_PRODUCTS,
17086
17318
  validation.data,
17087
17319
  mcpTokenPayload?.userId
17088
17320
  );
17089
- const route = await resolveProductCheckoutRoute(
17090
- sandboxBackendClient,
17091
- liveBackendClient,
17092
- validation.data.merchantId
17093
- );
17321
+ const route = await resolveProductCheckoutRouteForTool(validation.data.merchantId);
17094
17322
  if ("error" in route) {
17095
17323
  return toolError(route.error);
17096
17324
  }
@@ -17110,7 +17338,7 @@ function registerTools(server, options2) {
17110
17338
  server.registerTool(
17111
17339
  MCP_TOOL_NAMES.GET_CHECKOUT_CUSTOMIZATION,
17112
17340
  {
17113
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.GET_CHECKOUT_CUSTOMIZATION],
17341
+ description: toolDescriptions[MCP_TOOL_NAMES.GET_CHECKOUT_CUSTOMIZATION],
17114
17342
  annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.GET_CHECKOUT_CUSTOMIZATION],
17115
17343
  inputSchema: getCheckoutCustomizationInputSchema,
17116
17344
  outputSchema: checkoutCustomizationOutputSchema
@@ -17122,19 +17350,16 @@ function registerTools(server, options2) {
17122
17350
  if (!validation.ok) {
17123
17351
  return toolError(validation.errorText);
17124
17352
  }
17125
- if (!isMerchantAllowed(validation.data.merchantId, mcpTokenPayload)) {
17126
- return toolError("Merchant not authorized for this MCP key.");
17353
+ const getCheckoutDenial = await merchantAuthorizationDenial(validation.data.merchantId);
17354
+ if (getCheckoutDenial) {
17355
+ return getCheckoutDenial;
17127
17356
  }
17128
17357
  logMcpToolCallAfterAuth(
17129
17358
  MCP_TOOL_NAMES.GET_CHECKOUT_CUSTOMIZATION,
17130
17359
  validation.data,
17131
17360
  mcpTokenPayload?.userId
17132
17361
  );
17133
- const route = await resolveProductCheckoutRoute(
17134
- sandboxBackendClient,
17135
- liveBackendClient,
17136
- validation.data.merchantId
17137
- );
17362
+ const route = await resolveProductCheckoutRouteForTool(validation.data.merchantId);
17138
17363
  if ("error" in route) {
17139
17364
  return toolError(route.error);
17140
17365
  }
@@ -17154,7 +17379,7 @@ function registerTools(server, options2) {
17154
17379
  server.registerTool(
17155
17380
  MCP_TOOL_NAMES.UPDATE_CHECKOUT_CUSTOMIZATION,
17156
17381
  {
17157
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.UPDATE_CHECKOUT_CUSTOMIZATION],
17382
+ description: toolDescriptions[MCP_TOOL_NAMES.UPDATE_CHECKOUT_CUSTOMIZATION],
17158
17383
  annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.UPDATE_CHECKOUT_CUSTOMIZATION],
17159
17384
  inputSchema: updateCheckoutCustomizationInputSchema,
17160
17385
  outputSchema: checkoutCustomizationOutputSchema
@@ -17166,19 +17391,16 @@ function registerTools(server, options2) {
17166
17391
  if (!validation.ok) {
17167
17392
  return toolError(validation.errorText);
17168
17393
  }
17169
- if (!isMerchantAllowed(validation.data.merchantId, mcpTokenPayload)) {
17170
- return toolError("Merchant not authorized for this MCP key.");
17394
+ const updateCheckoutDenial = await merchantAuthorizationDenial(validation.data.merchantId);
17395
+ if (updateCheckoutDenial) {
17396
+ return updateCheckoutDenial;
17171
17397
  }
17172
17398
  logMcpToolCallAfterAuth(
17173
17399
  MCP_TOOL_NAMES.UPDATE_CHECKOUT_CUSTOMIZATION,
17174
17400
  validation.data,
17175
17401
  mcpTokenPayload?.userId
17176
17402
  );
17177
- const route = await resolveProductCheckoutRoute(
17178
- sandboxBackendClient,
17179
- liveBackendClient,
17180
- validation.data.merchantId
17181
- );
17403
+ const route = await resolveProductCheckoutRouteForTool(validation.data.merchantId);
17182
17404
  if ("error" in route) {
17183
17405
  return toolError(route.error);
17184
17406
  }
@@ -17198,7 +17420,7 @@ function registerTools(server, options2) {
17198
17420
  server.registerTool(
17199
17421
  MCP_TOOL_NAMES.SET_CHECKOUT_LOGO,
17200
17422
  {
17201
- description: TOOL_DESCRIPTIONS[MCP_TOOL_NAMES.SET_CHECKOUT_LOGO],
17423
+ description: toolDescriptions[MCP_TOOL_NAMES.SET_CHECKOUT_LOGO],
17202
17424
  annotations: TOOL_ANNOTATIONS[MCP_TOOL_NAMES.SET_CHECKOUT_LOGO],
17203
17425
  inputSchema: setCheckoutLogoInputSchema,
17204
17426
  outputSchema: setCheckoutLogoOutputSchema
@@ -17210,20 +17432,22 @@ function registerTools(server, options2) {
17210
17432
  if (!validation.ok) {
17211
17433
  return toolError(validation.errorText);
17212
17434
  }
17213
- if (!isMerchantAllowed(validation.data.merchantId, mcpTokenPayload)) {
17214
- return toolError("Merchant not authorized for this MCP key.");
17435
+ const setLogoDenial = await merchantAuthorizationDenial(validation.data.merchantId);
17436
+ if (setLogoDenial) {
17437
+ return setLogoDenial;
17215
17438
  }
17216
17439
  logMcpToolCallAfterAuth(
17217
17440
  MCP_TOOL_NAMES.SET_CHECKOUT_LOGO,
17218
17441
  validation.data,
17219
17442
  mcpTokenPayload?.userId
17220
17443
  );
17221
- if (!sandboxBackendClient) {
17444
+ const logoClient = deploymentRouter2 && dualBackendClients ? deploymentRouter2.checkoutLogoClient(dualBackendClients) : sandboxBackendClient;
17445
+ if (!logoClient) {
17222
17446
  return toolError(
17223
17447
  "The PAI Managed backend is not reachable. Contact your platform administrator to configure backend connectivity."
17224
17448
  );
17225
17449
  }
17226
- const result = await runSetCheckoutLogoHandler(sandboxBackendClient, validation.data);
17450
+ const result = await runSetCheckoutLogoHandler(logoClient, validation.data);
17227
17451
  if (!result.ok) {
17228
17452
  return toolError(result.errorText);
17229
17453
  }
@@ -17277,6 +17501,7 @@ function mcpHttpRequestLogger(req, res, next) {
17277
17501
  }
17278
17502
 
17279
17503
  // src/http/mcp-server-card.ts
17504
+ import { getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/express";
17280
17505
  var DEFAULT_DOCS_BASE_URL = "https://doc.managed.payments.ai";
17281
17506
  var DEFAULT_WEBSITE_URL = "https://managed.payments.ai";
17282
17507
  var MCP_PROTOCOL_VERSION = "2025-06-18";
@@ -17285,9 +17510,17 @@ function buildMcpServerCard(opts) {
17285
17510
  const websiteUrl2 = (opts.websiteUrl ?? DEFAULT_WEBSITE_URL).replace(/\/$/, "");
17286
17511
  const issuer = opts.oauthIssuerUrl.replace(/\/$/, "");
17287
17512
  const mcpEndpoint = opts.mcpPublicUrl ?? `${issuer}/api/mcp`;
17288
- const tools = Object.values(MCP_TOOL_NAMES).map((toolName) => ({
17513
+ const resourceMetadataUrl = opts.mcpPublicUrl ? getOAuthProtectedResourceMetadataUrl(new URL(opts.mcpPublicUrl)) : `${issuer}/.well-known/oauth-protected-resource/api/mcp`;
17514
+ const toolDescriptions = resolveToolDescriptions(opts.deploymentRouter?.mode ?? "dual");
17515
+ const toolNames = Object.values(MCP_TOOL_NAMES).filter((toolName) => {
17516
+ if (toolName === MCP_TOOL_NAMES.GO_LIVE && opts.deploymentRouter && !opts.deploymentRouter.supportsGoLive) {
17517
+ return false;
17518
+ }
17519
+ return true;
17520
+ });
17521
+ const tools = toolNames.map((toolName) => ({
17289
17522
  name: toolName,
17290
- description: TOOL_DESCRIPTIONS[toolName] ?? toolName
17523
+ description: toolDescriptions[toolName] ?? toolName
17291
17524
  }));
17292
17525
  return {
17293
17526
  serverInfo: {
@@ -17310,7 +17543,7 @@ function buildMcpServerCard(opts) {
17310
17543
  types: ["oauth2", "api_key"],
17311
17544
  oauth2: {
17312
17545
  authorization_server: `${issuer}/.well-known/oauth-authorization-server`,
17313
- resource_metadata: `${issuer}/.well-known/oauth-protected-resource/api/mcp`,
17546
+ resource_metadata: resourceMetadataUrl,
17314
17547
  required_scopes: [...SUPPORTED_OAUTH_SCOPES]
17315
17548
  },
17316
17549
  api_key: {
@@ -17440,9 +17673,12 @@ function createExpressApp(options2) {
17440
17673
  docsBaseUrl: docsBaseUrl2,
17441
17674
  websiteUrl: websiteUrl2,
17442
17675
  checkoutBaseUrl: checkoutBaseUrl2,
17443
- allowedMcpOriginHostnames = DEFAULT_ALLOWED_MCP_ORIGIN_HOSTNAMES
17676
+ allowedMcpOriginHostnames = DEFAULT_ALLOWED_MCP_ORIGIN_HOSTNAMES,
17677
+ deploymentRouter: deploymentRouterOption
17444
17678
  } = options2;
17445
- const liveBackendClient = backendClients2?.live;
17679
+ const deploymentRouter2 = deploymentRouterOption ?? (backendClients2 && hasLiveBackend(backendClients2) ? createDualRouter(backendClients2) : void 0);
17680
+ const tokenValidationClient = deploymentRouter2?.tokenValidationClient ?? backendClients2?.live ?? backendClients2?.sandbox;
17681
+ const healthProbeClient = deploymentRouter2?.healthProbeClient ?? backendClients2?.live;
17446
17682
  const app2 = createMcpExpressApp(resolveCreateMcpExpressAppOptions(host2, allowedHosts2));
17447
17683
  app2.use(
17448
17684
  cors({
@@ -17488,7 +17724,8 @@ function createExpressApp(options2) {
17488
17724
  oauthIssuerUrl: oauthIssuerUrl2,
17489
17725
  mcpPublicUrl: mcpPublicUrl2,
17490
17726
  docsBaseUrl: docsBaseUrl2,
17491
- websiteUrl: websiteUrl2
17727
+ websiteUrl: websiteUrl2,
17728
+ deploymentRouter: deploymentRouter2
17492
17729
  })
17493
17730
  )
17494
17731
  );
@@ -17497,11 +17734,11 @@ function createExpressApp(options2) {
17497
17734
  app2.get("/.well-known/mcp.json", sendMcpServerCard);
17498
17735
  }
17499
17736
  app2.get("/health", async (_req, res) => {
17500
- if (!liveBackendClient) {
17737
+ if (!healthProbeClient) {
17501
17738
  res.status(200).type("application/json").send(JSON.stringify({ status: "ok" }));
17502
17739
  return;
17503
17740
  }
17504
- const result = await liveBackendClient.get(BACKEND_HTTP_HEALTH_PATH);
17741
+ const result = await healthProbeClient.get(BACKEND_HTTP_HEALTH_PATH);
17505
17742
  if (result.ok) {
17506
17743
  res.status(200).type("application/json").send(JSON.stringify({ status: "ok", backend: { status: "ok" } }));
17507
17744
  } else {
@@ -17520,7 +17757,7 @@ function createExpressApp(options2) {
17520
17757
  );
17521
17758
  }
17522
17759
  });
17523
- const resourceMetadataUrl = mcpPublicUrl2 ? getOAuthProtectedResourceMetadataUrl(new URL(mcpPublicUrl2)) : void 0;
17760
+ const resourceMetadataUrl = mcpPublicUrl2 ? getOAuthProtectedResourceMetadataUrl2(new URL(mcpPublicUrl2)) : void 0;
17524
17761
  const mcpOriginMiddleware = createMcpOriginMiddleware(allowedMcpOriginHostnames);
17525
17762
  const sendUnauthorized = (res, message) => {
17526
17763
  if (resourceMetadataUrl) {
@@ -17533,7 +17770,7 @@ function createExpressApp(options2) {
17533
17770
  if (message) body.message = message;
17534
17771
  res.status(401).json(body);
17535
17772
  };
17536
- const mcpAuthMiddleware = liveBackendClient ? async (req, res, next) => {
17773
+ const mcpAuthMiddleware = tokenValidationClient ? async (req, res, next) => {
17537
17774
  const authHeader = req.headers.authorization;
17538
17775
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
17539
17776
  sendUnauthorized(res);
@@ -17545,7 +17782,7 @@ function createExpressApp(options2) {
17545
17782
  return;
17546
17783
  }
17547
17784
  try {
17548
- req.mcpTokenPayload = await validateMcpToken(token, liveBackendClient);
17785
+ req.mcpTokenPayload = await validateMcpToken(token, tokenValidationClient);
17549
17786
  req.auth = {
17550
17787
  token,
17551
17788
  clientId: req.mcpTokenPayload.userId,
@@ -17568,6 +17805,7 @@ function createExpressApp(options2) {
17568
17805
  const server = createInstrumentedMcpServer({ version: version2 });
17569
17806
  registerTools(server, {
17570
17807
  backendClients: backendClients2,
17808
+ deploymentRouter: deploymentRouter2,
17571
17809
  mcpTokenPayload: readMcpTokenPayload(authInfo),
17572
17810
  checkoutBaseUrl: checkoutBaseUrl2,
17573
17811
  dashboardUrl: websiteUrl2
@@ -17589,7 +17827,13 @@ function createExpressApp(options2) {
17589
17827
  }
17590
17828
  });
17591
17829
  app2.post(mcpPath2, mcpOriginMiddleware, mcpAuthMiddleware, async (req, res) => {
17592
- await handleMcpRequest(req, res, req.body);
17830
+ try {
17831
+ await handleMcpRequest(req, res, req.body);
17832
+ } finally {
17833
+ if (isSentryEnabled()) {
17834
+ await Sentry.flush(2e3);
17835
+ }
17836
+ }
17593
17837
  });
17594
17838
  const methodNotAllowed = (_req, res) => {
17595
17839
  res.status(405).set("Allow", "POST").type("application/json").send(
@@ -17609,8 +17853,10 @@ function createExpressApp(options2) {
17609
17853
  // src/main.http.ts
17610
17854
  var { host, port, mcpPath, allowedHosts, mcpPublicUrl, oauthIssuerUrl, docsBaseUrl, websiteUrl } = readMcpHttpListenConfig();
17611
17855
  var corsOrigin = resolveCorsOrigin();
17612
- var backendUrlConfig = readRequiredDualBackendUrlConfig();
17856
+ var deploymentMode = readMcpDeploymentMode();
17857
+ var backendUrlConfig = readBackendUrlConfig(deploymentMode);
17613
17858
  var backendClients = createDualBackendClients(backendUrlConfig);
17859
+ var deploymentRouter = createMcpDeploymentRouter(backendClients, deploymentMode);
17614
17860
  var checkoutBaseUrl = resolveCheckoutBaseUrl();
17615
17861
  var app = createExpressApp({
17616
17862
  host,
@@ -17618,12 +17864,14 @@ var app = createExpressApp({
17618
17864
  allowedHosts,
17619
17865
  corsOrigin,
17620
17866
  backendClients,
17867
+ deploymentRouter,
17621
17868
  mcpPublicUrl,
17622
17869
  oauthIssuerUrl,
17623
17870
  docsBaseUrl,
17624
17871
  websiteUrl,
17625
17872
  checkoutBaseUrl
17626
17873
  });
17874
+ console.error(`[mcp-http] deploymentMode=${deploymentMode}`);
17627
17875
  var httpServer = app.listen(port, host, () => {
17628
17876
  console.error(`[mcp-http] listening host=${host} port=${port} mcpPath=${mcpPath}`);
17629
17877
  console.error(