@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.
- package/dist/main.http.js +1004 -756
- 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
|
-
|
|
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
|
-
|
|
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/
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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:
|
|
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/
|
|
15547
|
-
|
|
15548
|
-
|
|
15549
|
-
|
|
15550
|
-
|
|
15551
|
-
|
|
15552
|
-
|
|
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
|
|
15556
|
-
const parsed =
|
|
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
|
|
15591
|
+
errorText: `Invalid arguments for create_merchant_account.
|
|
15561
15592
|
${formatZodIssuesForTool(parsed.error.issues)}`
|
|
15562
15593
|
};
|
|
15563
15594
|
}
|
|
15564
|
-
return { ok: true, data:
|
|
15595
|
+
return { ok: true, data: buildCreateMerchantAccountStub(parsed.data) };
|
|
15565
15596
|
}
|
|
15566
|
-
|
|
15567
|
-
const
|
|
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:
|
|
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 =
|
|
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,
|
|
15591
|
-
}
|
|
15592
|
-
|
|
15593
|
-
|
|
15594
|
-
|
|
15595
|
-
|
|
15596
|
-
|
|
15597
|
-
|
|
15598
|
-
|
|
15599
|
-
|
|
15600
|
-
|
|
15601
|
-
|
|
15602
|
-
|
|
15603
|
-
|
|
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/
|
|
15714
|
-
var
|
|
15715
|
-
function
|
|
15716
|
-
const
|
|
15717
|
-
|
|
15718
|
-
|
|
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
|
-
|
|
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/
|
|
15725
|
-
|
|
15726
|
-
|
|
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
|
|
15736
|
-
|
|
15737
|
-
|
|
15738
|
-
|
|
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
|
|
15971
|
+
return { ok: true, data: buildActivationStatusStub(parsed.data) };
|
|
15741
15972
|
}
|
|
15742
|
-
async function
|
|
15743
|
-
const
|
|
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("
|
|
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
|
|
15761
|
-
if (!
|
|
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:
|
|
15997
|
+
return { ok: true, data: out.data };
|
|
15776
15998
|
}
|
|
15777
|
-
|
|
15778
|
-
|
|
15779
|
-
|
|
15780
|
-
|
|
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
|
|
15794
|
-
|
|
15795
|
-
|
|
15796
|
-
|
|
15797
|
-
|
|
15798
|
-
|
|
15799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15806
|
-
|
|
15807
|
-
|
|
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
|
|
16468
|
+
const mapped = mapBackendListProductsResponse(parsedJson);
|
|
16469
|
+
const out = listProductsOutputSchema.safeParse(mapped);
|
|
16380
16470
|
if (!out.success) {
|
|
16381
|
-
return {
|
|
16471
|
+
return {
|
|
16472
|
+
ok: false,
|
|
16473
|
+
errorText: UNEXPECTED_RESPONSE_ERROR
|
|
16474
|
+
};
|
|
16382
16475
|
}
|
|
16383
|
-
return { ok: true,
|
|
16476
|
+
return { ok: true, data: out.data };
|
|
16384
16477
|
}
|
|
16385
|
-
|
|
16386
|
-
|
|
16387
|
-
|
|
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
|
-
|
|
16390
|
-
|
|
16391
|
-
|
|
16392
|
-
|
|
16393
|
-
|
|
16394
|
-
|
|
16395
|
-
|
|
16396
|
-
|
|
16397
|
-
|
|
16398
|
-
|
|
16399
|
-
|
|
16400
|
-
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
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
|
-
|
|
16407
|
-
|
|
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
|
|
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
|
-
|
|
16769
|
-
|
|
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:
|
|
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 (!
|
|
17036
|
+
if (!dualBackendClients || !deploymentRouter2) {
|
|
16798
17037
|
return toolError(BACKEND_UNREACHABLE_ERROR);
|
|
16799
17038
|
}
|
|
16800
|
-
|
|
16801
|
-
|
|
16802
|
-
|
|
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:
|
|
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 =
|
|
16830
|
-
|
|
16831
|
-
|
|
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:
|
|
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
|
-
|
|
16861
|
-
|
|
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
|
-
|
|
16879
|
-
|
|
16880
|
-
|
|
16881
|
-
|
|
16882
|
-
|
|
16883
|
-
|
|
16884
|
-
|
|
16885
|
-
|
|
16886
|
-
|
|
16887
|
-
|
|
16888
|
-
|
|
16889
|
-
|
|
16890
|
-
|
|
16891
|
-
|
|
16892
|
-
|
|
16893
|
-
|
|
16894
|
-
|
|
16895
|
-
args,
|
|
16896
|
-
|
|
16897
|
-
|
|
16898
|
-
|
|
16899
|
-
return
|
|
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
|
-
|
|
16902
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
16946
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
16994
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
17038
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
17082
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
17126
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
17170
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
17214
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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 (!
|
|
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
|
|
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 ?
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
|
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(
|