@solvapay/server 1.0.1-preview.1 → 1.0.1-preview.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/index.js CHANGED
@@ -920,10 +920,10 @@ async function createAdapterHandler(adapter, paywall, metadata, businessLogic) {
920
920
  const backendRefCache = /* @__PURE__ */ new Map();
921
921
  const getCustomerRef = (args) => args.auth?.customer_ref || "anonymous";
922
922
  const protectedHandler = await paywall.protect(businessLogic, metadata, getCustomerRef);
923
- return async (context) => {
923
+ return async (context, extra) => {
924
924
  try {
925
925
  const args = await adapter.extractArgs(context);
926
- const customerRef = await adapter.getCustomerRef(context);
926
+ const customerRef = await adapter.getCustomerRef(context, extra);
927
927
  let backendRef = backendRefCache.get(customerRef);
928
928
  if (!backendRef) {
929
929
  backendRef = await paywall.ensureCustomer(customerRef, customerRef);
@@ -1114,18 +1114,18 @@ var McpAdapter = class {
1114
1114
  extractArgs(args) {
1115
1115
  return args;
1116
1116
  }
1117
- async getCustomerRef(args) {
1117
+ async getCustomerRef(args, extra) {
1118
1118
  if (this.options.getCustomerRef) {
1119
- const ref = await this.options.getCustomerRef(args);
1119
+ const ref = await this.options.getCustomerRef(args, extra);
1120
1120
  return AdapterUtils.ensureCustomerRef(ref);
1121
1121
  }
1122
- const auth = args?.auth;
1123
- const customerRef = auth?.customer_ref || "anonymous";
1122
+ const customerRefFromExtra = extra?.authInfo?.extra?.customer_ref;
1123
+ const customerRef = typeof customerRefFromExtra === "string" && customerRefFromExtra.trim() ? customerRefFromExtra.trim() : "anonymous";
1124
1124
  return AdapterUtils.ensureCustomerRef(customerRef);
1125
1125
  }
1126
1126
  formatResponse(result, _context) {
1127
1127
  const transformed = this.options.transformResponse ? this.options.transformResponse(result) : result;
1128
- return {
1128
+ const response = {
1129
1129
  content: [
1130
1130
  {
1131
1131
  type: "text",
@@ -1133,6 +1133,10 @@ var McpAdapter = class {
1133
1133
  }
1134
1134
  ]
1135
1135
  };
1136
+ if (transformed && typeof transformed === "object" && !Array.isArray(transformed)) {
1137
+ response.structuredContent = transformed;
1138
+ }
1139
+ return response;
1136
1140
  }
1137
1141
  formatError(error, _context) {
1138
1142
  if (error instanceof PaywallError) {
@@ -1220,8 +1224,8 @@ function mcpErrorResult(message) {
1220
1224
  return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
1221
1225
  }
1222
1226
  function createGetUserInfoHandler(apiClient, productRef, getCustomerRef) {
1223
- return async (args) => {
1224
- const customerRef = getCustomerRef(args);
1227
+ return async (args, extra) => {
1228
+ const customerRef = getCustomerRef(args, extra);
1225
1229
  try {
1226
1230
  if (!apiClient.getUserInfo) {
1227
1231
  return mcpErrorResult("getUserInfo is not available on this API client");
@@ -1236,12 +1240,12 @@ function createGetUserInfoHandler(apiClient, productRef, getCustomerRef) {
1236
1240
  };
1237
1241
  }
1238
1242
  function createUpgradeHandler(apiClient, productRef, getCustomerRef) {
1239
- return async (args) => {
1240
- const customerRef = getCustomerRef(args);
1243
+ return async (args, extra) => {
1244
+ const customerRef = getCustomerRef(args, extra);
1241
1245
  const planRef = typeof args.planRef === "string" ? args.planRef : void 0;
1242
1246
  try {
1243
1247
  const result = await apiClient.createCheckoutSession({
1244
- customerReference: customerRef,
1248
+ customerRef,
1245
1249
  productRef,
1246
1250
  ...planRef && { planRef }
1247
1251
  });
@@ -1268,8 +1272,8 @@ You'll be able to compare options and select the one that's right for you.`;
1268
1272
  };
1269
1273
  }
1270
1274
  function createManageAccountHandler(apiClient, productRef, getCustomerRef) {
1271
- return async (args) => {
1272
- const customerRef = getCustomerRef(args);
1275
+ return async (args, extra) => {
1276
+ const customerRef = getCustomerRef(args, extra);
1273
1277
  try {
1274
1278
  const session = await apiClient.createCustomerSession({ customerRef, productRef });
1275
1279
  const portalUrl = session.customerUrl;
@@ -1312,6 +1316,98 @@ function createVirtualTools(apiClient, options) {
1312
1316
  return allTools.filter((t) => !excludeSet.has(t.name));
1313
1317
  }
1314
1318
 
1319
+ // src/register-virtual-tools-mcp.ts
1320
+ import { createRequire } from "module";
1321
+ function getNodeRequire() {
1322
+ try {
1323
+ return (0, eval)("require");
1324
+ } catch {
1325
+ return createRequire(`${process.cwd()}/package.json`);
1326
+ }
1327
+ }
1328
+ var nodeRequire = getNodeRequire();
1329
+ function getZod() {
1330
+ try {
1331
+ const zodModule = nodeRequire("zod");
1332
+ return zodModule.z ?? zodModule;
1333
+ } catch {
1334
+ throw new Error(
1335
+ "zod is required to use registerVirtualToolsMcp(). Install it as a dependency in your MCP server project."
1336
+ );
1337
+ }
1338
+ }
1339
+ function isJsonSchemaObject(value) {
1340
+ return Boolean(value && typeof value === "object");
1341
+ }
1342
+ function toZodSchema(property) {
1343
+ const z = getZod();
1344
+ if (Array.isArray(property.enum) && property.enum.length > 0) {
1345
+ if (property.enum.every((value) => typeof value === "string")) {
1346
+ const [first, ...rest] = property.enum;
1347
+ return z.enum([first, ...rest]);
1348
+ }
1349
+ if (property.enum.length === 1) {
1350
+ return z.literal(property.enum[0]);
1351
+ }
1352
+ return z.union(property.enum.map((value) => z.literal(value)));
1353
+ }
1354
+ switch (property.type) {
1355
+ case "string":
1356
+ return z.string();
1357
+ case "number":
1358
+ return z.number();
1359
+ case "integer":
1360
+ return z.number().int();
1361
+ case "boolean":
1362
+ return z.boolean();
1363
+ case "array":
1364
+ return z.array(
1365
+ isJsonSchemaObject(property.items) ? toZodSchema(property.items) : z.any()
1366
+ );
1367
+ case "object":
1368
+ return z.object(jsonSchemaToZodRawShape(property.properties || {}));
1369
+ default:
1370
+ return z.any();
1371
+ }
1372
+ }
1373
+ function jsonSchemaToZodRawShape(properties, required = []) {
1374
+ const requiredSet = new Set(required);
1375
+ return Object.fromEntries(
1376
+ Object.entries(properties).map(([name, property]) => {
1377
+ const schema = toZodSchema(property);
1378
+ const finalSchema = requiredSet.has(name) ? schema : schema.optional();
1379
+ return [name, finalSchema];
1380
+ })
1381
+ );
1382
+ }
1383
+ function defaultGetCustomerRef(_args, extra) {
1384
+ const fromExtra = extra?.authInfo?.extra?.customer_ref;
1385
+ return typeof fromExtra === "string" && fromExtra.trim() ? fromExtra.trim() : "anonymous";
1386
+ }
1387
+ function registerVirtualToolsMcpImpl(server, apiClient, options) {
1388
+ const { filter, mapDefinition, wrapHandler, getCustomerRef, ...virtualToolsOptions } = options;
1389
+ const virtualTools = createVirtualTools(apiClient, {
1390
+ ...virtualToolsOptions,
1391
+ getCustomerRef: getCustomerRef || defaultGetCustomerRef
1392
+ });
1393
+ for (const toolDefinition of virtualTools) {
1394
+ if (filter && !filter(toolDefinition)) continue;
1395
+ const mappedDefinition = mapDefinition ? mapDefinition(toolDefinition) : toolDefinition;
1396
+ const wrappedHandler = wrapHandler ? wrapHandler(mappedDefinition.handler, mappedDefinition) : mappedDefinition.handler;
1397
+ server.registerTool(
1398
+ mappedDefinition.name,
1399
+ {
1400
+ description: mappedDefinition.description,
1401
+ inputSchema: jsonSchemaToZodRawShape(
1402
+ mappedDefinition.inputSchema.properties,
1403
+ mappedDefinition.inputSchema.required || []
1404
+ )
1405
+ },
1406
+ wrappedHandler
1407
+ );
1408
+ }
1409
+ }
1410
+
1315
1411
  // src/factory.ts
1316
1412
  function createSolvaPay(config) {
1317
1413
  let resolvedConfig;
@@ -1368,7 +1464,7 @@ function createSolvaPay(config) {
1368
1464
  },
1369
1465
  createCheckoutSession(params) {
1370
1466
  return apiClient.createCheckoutSession({
1371
- customerReference: params.customerRef,
1467
+ customerRef: params.customerRef,
1372
1468
  productRef: params.productRef,
1373
1469
  planRef: params.planRef
1374
1470
  });
@@ -1385,6 +1481,9 @@ function createSolvaPay(config) {
1385
1481
  getVirtualTools(options) {
1386
1482
  return createVirtualTools(apiClient, options);
1387
1483
  },
1484
+ async registerVirtualToolsMcp(server, options) {
1485
+ await registerVirtualToolsMcpImpl(server, apiClient, options);
1486
+ },
1388
1487
  // Payable API for framework-specific handlers
1389
1488
  payable(options = {}) {
1390
1489
  const product = options.productRef || options.product || process.env.SOLVAPAY_PRODUCT || "default-product";
@@ -1423,9 +1522,9 @@ function createSolvaPay(config) {
1423
1522
  getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
1424
1523
  });
1425
1524
  const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
1426
- return async (args) => {
1525
+ return async (args, extra) => {
1427
1526
  const handler = await handlerPromise;
1428
- return handler(args);
1527
+ return handler(args, extra);
1429
1528
  };
1430
1529
  },
1431
1530
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1495,6 +1594,167 @@ function getCustomerRefFromBearerAuthHeader(authorization, options = {}) {
1495
1594
  return getCustomerRefFromJwtPayload(payload, options);
1496
1595
  }
1497
1596
 
1597
+ // src/mcp/auth-bridge.ts
1598
+ function getClientId(payload, explicitClientId) {
1599
+ if (explicitClientId) return explicitClientId;
1600
+ const payloadClientId = typeof payload.client_id === "string" && payload.client_id || typeof payload.azp === "string" && payload.azp || typeof payload.aud === "string" && payload.aud || null;
1601
+ return payloadClientId || "solvapay-mcp-client";
1602
+ }
1603
+ function getScopes(payload, defaultScopes) {
1604
+ if (Array.isArray(payload.scp)) {
1605
+ return payload.scp.filter((scope) => typeof scope === "string");
1606
+ }
1607
+ if (typeof payload.scope === "string" && payload.scope.trim()) {
1608
+ return payload.scope.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
1609
+ }
1610
+ return defaultScopes;
1611
+ }
1612
+ function getExpiresAt(payload) {
1613
+ return typeof payload.exp === "number" ? payload.exp : void 0;
1614
+ }
1615
+ function buildAuthInfoFromBearer(authorization, options = {}) {
1616
+ const token = extractBearerToken(authorization);
1617
+ if (!token) return null;
1618
+ const payload = decodeJwtPayload(token);
1619
+ const customerRef = getCustomerRefFromJwtPayload(payload, options);
1620
+ const clientId = getClientId(payload, options.clientId);
1621
+ const scopes = getScopes(payload, options.defaultScopes || []);
1622
+ const expiresAt = getExpiresAt(payload);
1623
+ return {
1624
+ token,
1625
+ clientId,
1626
+ scopes,
1627
+ expiresAt,
1628
+ extra: {
1629
+ customer_ref: customerRef,
1630
+ ...options.includePayload ? { payload } : {}
1631
+ }
1632
+ };
1633
+ }
1634
+
1635
+ // src/mcp/oauth-bridge.ts
1636
+ function withoutTrailingSlash(value) {
1637
+ return value.replace(/\/$/, "");
1638
+ }
1639
+ function getRequestAuthHeader(req) {
1640
+ const header = req.headers?.authorization;
1641
+ if (typeof header === "string") return header;
1642
+ if (Array.isArray(header)) return header[0] || null;
1643
+ return null;
1644
+ }
1645
+ function getRequestJsonRpcId(body) {
1646
+ if (body && typeof body === "object" && "id" in body) {
1647
+ const id = body.id;
1648
+ return id ?? null;
1649
+ }
1650
+ return null;
1651
+ }
1652
+ function makeUnauthorizedJsonRpc(id) {
1653
+ return {
1654
+ jsonrpc: "2.0",
1655
+ id,
1656
+ error: {
1657
+ code: -32001,
1658
+ message: "Unauthorized"
1659
+ }
1660
+ };
1661
+ }
1662
+ function setMcpChallengeHeader(res, publicBaseUrl, protectedResourcePath) {
1663
+ res.setHeader(
1664
+ "WWW-Authenticate",
1665
+ `Bearer resource_metadata="${withoutTrailingSlash(publicBaseUrl)}${protectedResourcePath}"`
1666
+ );
1667
+ }
1668
+ function getOAuthProtectedResourceResponse(publicBaseUrl) {
1669
+ const resource = withoutTrailingSlash(publicBaseUrl);
1670
+ return {
1671
+ resource,
1672
+ authorization_servers: [resource],
1673
+ scopes_supported: ["openid", "profile", "email"]
1674
+ };
1675
+ }
1676
+ function getOAuthAuthorizationServerResponse({
1677
+ apiBaseUrl,
1678
+ productRef
1679
+ }) {
1680
+ const normalizedApiBaseUrl = withoutTrailingSlash(apiBaseUrl);
1681
+ const registrationEndpoint = `${normalizedApiBaseUrl}/v1/customer/auth/register?product_ref=${encodeURIComponent(productRef)}`;
1682
+ return {
1683
+ issuer: normalizedApiBaseUrl,
1684
+ authorization_endpoint: `${normalizedApiBaseUrl}/v1/customer/auth/authorize`,
1685
+ token_endpoint: `${normalizedApiBaseUrl}/v1/customer/auth/token`,
1686
+ registration_endpoint: registrationEndpoint,
1687
+ token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
1688
+ response_types_supported: ["code"],
1689
+ grant_types_supported: ["authorization_code", "refresh_token"],
1690
+ scopes_supported: ["openid", "profile", "email"],
1691
+ code_challenge_methods_supported: ["S256"]
1692
+ };
1693
+ }
1694
+ function createMcpOAuthBridge(options) {
1695
+ const {
1696
+ publicBaseUrl,
1697
+ apiBaseUrl,
1698
+ productRef,
1699
+ mcpPath = "/mcp",
1700
+ requireAuth = true,
1701
+ authInfo,
1702
+ protectedResourcePath = "/.well-known/oauth-protected-resource",
1703
+ authorizationServerPath = "/.well-known/oauth-authorization-server"
1704
+ } = options;
1705
+ const protectedResourceMiddleware = (req, res, next) => {
1706
+ if (req.method !== "GET" || req.path !== protectedResourcePath) {
1707
+ next();
1708
+ return;
1709
+ }
1710
+ res.json(getOAuthProtectedResourceResponse(publicBaseUrl));
1711
+ };
1712
+ const authorizationServerMiddleware = (req, res, next) => {
1713
+ if (req.method !== "GET" || req.path !== authorizationServerPath) {
1714
+ next();
1715
+ return;
1716
+ }
1717
+ if (!productRef) {
1718
+ res.status(500).json({ error: "SOLVAPAY_PRODUCT_REF missing" });
1719
+ return;
1720
+ }
1721
+ res.json(
1722
+ getOAuthAuthorizationServerResponse({
1723
+ apiBaseUrl,
1724
+ productRef
1725
+ })
1726
+ );
1727
+ };
1728
+ const mcpAuthMiddleware = (req, res, next) => {
1729
+ if (req.path !== mcpPath) {
1730
+ next();
1731
+ return;
1732
+ }
1733
+ const authHeader = getRequestAuthHeader(req);
1734
+ const id = getRequestJsonRpcId(req.body);
1735
+ if (!authHeader && !requireAuth) {
1736
+ next();
1737
+ return;
1738
+ }
1739
+ try {
1740
+ const auth = buildAuthInfoFromBearer(authHeader, authInfo);
1741
+ if (!auth) {
1742
+ throw new McpBearerAuthError("Missing bearer token");
1743
+ }
1744
+ req.auth = auth;
1745
+ next();
1746
+ } catch {
1747
+ setMcpChallengeHeader(res, publicBaseUrl, protectedResourcePath);
1748
+ if (req.method === "POST") {
1749
+ res.status(401).json(makeUnauthorizedJsonRpc(id));
1750
+ return;
1751
+ }
1752
+ res.status(401).json({ error: "Unauthorized" });
1753
+ }
1754
+ };
1755
+ return [protectedResourceMiddleware, authorizationServerMiddleware, mcpAuthMiddleware];
1756
+ }
1757
+
1498
1758
  // src/helpers/error.ts
1499
1759
  import { SolvaPayError as SolvaPayError3 } from "@solvapay/core";
1500
1760
  function isErrorResult(result) {
@@ -1848,9 +2108,11 @@ export {
1848
2108
  McpBearerAuthError,
1849
2109
  PaywallError,
1850
2110
  VIRTUAL_TOOL_DEFINITIONS,
2111
+ buildAuthInfoFromBearer,
1851
2112
  cancelPurchaseCore,
1852
2113
  createCheckoutSessionCore,
1853
2114
  createCustomerSessionCore,
2115
+ createMcpOAuthBridge,
1854
2116
  createPaymentIntentCore,
1855
2117
  createSolvaPay,
1856
2118
  createSolvaPayClient,
@@ -1860,10 +2122,14 @@ export {
1860
2122
  getAuthenticatedUserCore,
1861
2123
  getCustomerRefFromBearerAuthHeader,
1862
2124
  getCustomerRefFromJwtPayload,
2125
+ getOAuthAuthorizationServerResponse,
2126
+ getOAuthProtectedResourceResponse,
1863
2127
  handleRouteError,
1864
2128
  isErrorResult,
2129
+ jsonSchemaToZodRawShape,
1865
2130
  listPlansCore,
1866
2131
  processPaymentIntentCore,
2132
+ registerVirtualToolsMcpImpl,
1867
2133
  syncCustomerCore,
1868
2134
  verifyWebhook,
1869
2135
  withRetry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solvapay/server",
3
- "version": "1.0.1-preview.1",
3
+ "version": "1.0.1-preview.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -37,10 +37,20 @@
37
37
  },
38
38
  "sideEffects": false,
39
39
  "dependencies": {
40
- "@solvapay/core": "1.0.1-preview.1"
40
+ "@solvapay/core": "1.0.1-preview.3"
41
41
  },
42
42
  "peerDependencies": {
43
- "@solvapay/auth": "1.0.1-preview.1"
43
+ "@modelcontextprotocol/sdk": "^1.28.0",
44
+ "zod": "^3.25.0 || ^4.0.0",
45
+ "@solvapay/auth": "1.0.1-preview.3"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "@modelcontextprotocol/sdk": {
49
+ "optional": true
50
+ },
51
+ "zod": {
52
+ "optional": true
53
+ }
44
54
  },
45
55
  "devDependencies": {
46
56
  "dotenv": "^17.3.1",
@@ -52,7 +62,7 @@
52
62
  "vitest": "^4.1.2",
53
63
  "@solvapay/demo-services": "0.0.0",
54
64
  "@solvapay/test-utils": "^0.0.0",
55
- "@solvapay/auth": "1.0.1-preview.1"
65
+ "@solvapay/auth": "1.0.1-preview.3"
56
66
  },
57
67
  "scripts": {
58
68
  "build": "tsup src/index.ts --format esm,cjs --dts --tsconfig tsconfig.build.json && tsup src/edge.ts --format esm --dts --tsconfig tsconfig.build.json",