@solvapay/server 1.0.2 → 1.0.4

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
@@ -163,6 +163,21 @@ function createSolvaPayClient(opts) {
163
163
  }
164
164
  return await res.json();
165
165
  },
166
+ // PUT: /v1/sdk/products/{productRef}/mcp/plans
167
+ async configureMcpPlans(productRef, params) {
168
+ const url = `${base}/v1/sdk/products/${productRef}/mcp/plans`;
169
+ const res = await fetch(url, {
170
+ method: "PUT",
171
+ headers,
172
+ body: JSON.stringify(params)
173
+ });
174
+ if (!res.ok) {
175
+ const error = await res.text();
176
+ log(`\u274C API Error: ${res.status} - ${error}`);
177
+ throw new SolvaPayError(`Configure MCP plans failed (${res.status}): ${error}`);
178
+ }
179
+ return await res.json();
180
+ },
166
181
  // DELETE: /v1/sdk/products/{productRef}
167
182
  async deleteProduct(productRef) {
168
183
  const url = `${base}/v1/sdk/products/${productRef}`;
@@ -920,10 +935,10 @@ async function createAdapterHandler(adapter, paywall, metadata, businessLogic) {
920
935
  const backendRefCache = /* @__PURE__ */ new Map();
921
936
  const getCustomerRef = (args) => args.auth?.customer_ref || "anonymous";
922
937
  const protectedHandler = await paywall.protect(businessLogic, metadata, getCustomerRef);
923
- return async (context) => {
938
+ return async (context, extra) => {
924
939
  try {
925
940
  const args = await adapter.extractArgs(context);
926
- const customerRef = await adapter.getCustomerRef(context);
941
+ const customerRef = await adapter.getCustomerRef(context, extra);
927
942
  let backendRef = backendRefCache.get(customerRef);
928
943
  if (!backendRef) {
929
944
  backendRef = await paywall.ensureCustomer(customerRef, customerRef);
@@ -1114,18 +1129,18 @@ var McpAdapter = class {
1114
1129
  extractArgs(args) {
1115
1130
  return args;
1116
1131
  }
1117
- async getCustomerRef(args) {
1132
+ async getCustomerRef(args, extra) {
1118
1133
  if (this.options.getCustomerRef) {
1119
- const ref = await this.options.getCustomerRef(args);
1134
+ const ref = await this.options.getCustomerRef(args, extra);
1120
1135
  return AdapterUtils.ensureCustomerRef(ref);
1121
1136
  }
1122
- const auth = args?.auth;
1123
- const customerRef = auth?.customer_ref || "anonymous";
1137
+ const customerRefFromExtra = extra?.authInfo?.extra?.customer_ref;
1138
+ const customerRef = typeof customerRefFromExtra === "string" && customerRefFromExtra.trim() ? customerRefFromExtra.trim() : "anonymous";
1124
1139
  return AdapterUtils.ensureCustomerRef(customerRef);
1125
1140
  }
1126
1141
  formatResponse(result, _context) {
1127
1142
  const transformed = this.options.transformResponse ? this.options.transformResponse(result) : result;
1128
- return {
1143
+ const response = {
1129
1144
  content: [
1130
1145
  {
1131
1146
  type: "text",
@@ -1133,6 +1148,10 @@ var McpAdapter = class {
1133
1148
  }
1134
1149
  ]
1135
1150
  };
1151
+ if (transformed && typeof transformed === "object" && !Array.isArray(transformed)) {
1152
+ response.structuredContent = transformed;
1153
+ }
1154
+ return response;
1136
1155
  }
1137
1156
  formatError(error, _context) {
1138
1157
  if (error instanceof PaywallError) {
@@ -1220,8 +1239,8 @@ function mcpErrorResult(message) {
1220
1239
  return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
1221
1240
  }
1222
1241
  function createGetUserInfoHandler(apiClient, productRef, getCustomerRef) {
1223
- return async (args) => {
1224
- const customerRef = getCustomerRef(args);
1242
+ return async (args, extra) => {
1243
+ const customerRef = getCustomerRef(args, extra);
1225
1244
  try {
1226
1245
  if (!apiClient.getUserInfo) {
1227
1246
  return mcpErrorResult("getUserInfo is not available on this API client");
@@ -1236,12 +1255,12 @@ function createGetUserInfoHandler(apiClient, productRef, getCustomerRef) {
1236
1255
  };
1237
1256
  }
1238
1257
  function createUpgradeHandler(apiClient, productRef, getCustomerRef) {
1239
- return async (args) => {
1240
- const customerRef = getCustomerRef(args);
1258
+ return async (args, extra) => {
1259
+ const customerRef = getCustomerRef(args, extra);
1241
1260
  const planRef = typeof args.planRef === "string" ? args.planRef : void 0;
1242
1261
  try {
1243
1262
  const result = await apiClient.createCheckoutSession({
1244
- customerReference: customerRef,
1263
+ customerRef,
1245
1264
  productRef,
1246
1265
  ...planRef && { planRef }
1247
1266
  });
@@ -1268,8 +1287,8 @@ You'll be able to compare options and select the one that's right for you.`;
1268
1287
  };
1269
1288
  }
1270
1289
  function createManageAccountHandler(apiClient, productRef, getCustomerRef) {
1271
- return async (args) => {
1272
- const customerRef = getCustomerRef(args);
1290
+ return async (args, extra) => {
1291
+ const customerRef = getCustomerRef(args, extra);
1273
1292
  try {
1274
1293
  const session = await apiClient.createCustomerSession({ customerRef, productRef });
1275
1294
  const portalUrl = session.customerUrl;
@@ -1312,6 +1331,98 @@ function createVirtualTools(apiClient, options) {
1312
1331
  return allTools.filter((t) => !excludeSet.has(t.name));
1313
1332
  }
1314
1333
 
1334
+ // src/register-virtual-tools-mcp.ts
1335
+ import { createRequire } from "module";
1336
+ function getNodeRequire() {
1337
+ try {
1338
+ return (0, eval)("require");
1339
+ } catch {
1340
+ return createRequire(`${process.cwd()}/package.json`);
1341
+ }
1342
+ }
1343
+ var nodeRequire = getNodeRequire();
1344
+ function getZod() {
1345
+ try {
1346
+ const zodModule = nodeRequire("zod");
1347
+ return zodModule.z ?? zodModule;
1348
+ } catch {
1349
+ throw new Error(
1350
+ "zod is required to use registerVirtualToolsMcp(). Install it as a dependency in your MCP server project."
1351
+ );
1352
+ }
1353
+ }
1354
+ function isJsonSchemaObject(value) {
1355
+ return Boolean(value && typeof value === "object");
1356
+ }
1357
+ function toZodSchema(property) {
1358
+ const z = getZod();
1359
+ if (Array.isArray(property.enum) && property.enum.length > 0) {
1360
+ if (property.enum.every((value) => typeof value === "string")) {
1361
+ const [first, ...rest] = property.enum;
1362
+ return z.enum([first, ...rest]);
1363
+ }
1364
+ if (property.enum.length === 1) {
1365
+ return z.literal(property.enum[0]);
1366
+ }
1367
+ return z.union(property.enum.map((value) => z.literal(value)));
1368
+ }
1369
+ switch (property.type) {
1370
+ case "string":
1371
+ return z.string();
1372
+ case "number":
1373
+ return z.number();
1374
+ case "integer":
1375
+ return z.number().int();
1376
+ case "boolean":
1377
+ return z.boolean();
1378
+ case "array":
1379
+ return z.array(
1380
+ isJsonSchemaObject(property.items) ? toZodSchema(property.items) : z.any()
1381
+ );
1382
+ case "object":
1383
+ return z.object(jsonSchemaToZodRawShape(property.properties || {}));
1384
+ default:
1385
+ return z.any();
1386
+ }
1387
+ }
1388
+ function jsonSchemaToZodRawShape(properties, required = []) {
1389
+ const requiredSet = new Set(required);
1390
+ return Object.fromEntries(
1391
+ Object.entries(properties).map(([name, property]) => {
1392
+ const schema = toZodSchema(property);
1393
+ const finalSchema = requiredSet.has(name) ? schema : schema.optional();
1394
+ return [name, finalSchema];
1395
+ })
1396
+ );
1397
+ }
1398
+ function defaultGetCustomerRef(_args, extra) {
1399
+ const fromExtra = extra?.authInfo?.extra?.customer_ref;
1400
+ return typeof fromExtra === "string" && fromExtra.trim() ? fromExtra.trim() : "anonymous";
1401
+ }
1402
+ function registerVirtualToolsMcpImpl(server, apiClient, options) {
1403
+ const { filter, mapDefinition, wrapHandler, getCustomerRef, ...virtualToolsOptions } = options;
1404
+ const virtualTools = createVirtualTools(apiClient, {
1405
+ ...virtualToolsOptions,
1406
+ getCustomerRef: getCustomerRef || defaultGetCustomerRef
1407
+ });
1408
+ for (const toolDefinition of virtualTools) {
1409
+ if (filter && !filter(toolDefinition)) continue;
1410
+ const mappedDefinition = mapDefinition ? mapDefinition(toolDefinition) : toolDefinition;
1411
+ const wrappedHandler = wrapHandler ? wrapHandler(mappedDefinition.handler, mappedDefinition) : mappedDefinition.handler;
1412
+ server.registerTool(
1413
+ mappedDefinition.name,
1414
+ {
1415
+ description: mappedDefinition.description,
1416
+ inputSchema: jsonSchemaToZodRawShape(
1417
+ mappedDefinition.inputSchema.properties,
1418
+ mappedDefinition.inputSchema.required || []
1419
+ )
1420
+ },
1421
+ wrappedHandler
1422
+ );
1423
+ }
1424
+ }
1425
+
1315
1426
  // src/factory.ts
1316
1427
  function createSolvaPay(config) {
1317
1428
  let resolvedConfig;
@@ -1368,7 +1479,7 @@ function createSolvaPay(config) {
1368
1479
  },
1369
1480
  createCheckoutSession(params) {
1370
1481
  return apiClient.createCheckoutSession({
1371
- customerReference: params.customerRef,
1482
+ customerRef: params.customerRef,
1372
1483
  productRef: params.productRef,
1373
1484
  planRef: params.planRef
1374
1485
  });
@@ -1382,9 +1493,18 @@ function createSolvaPay(config) {
1382
1493
  }
1383
1494
  return apiClient.bootstrapMcpProduct(params);
1384
1495
  },
1496
+ configureMcpPlans(productRef, params) {
1497
+ if (!apiClient.configureMcpPlans) {
1498
+ throw new SolvaPayError2("configureMcpPlans is not available on this API client");
1499
+ }
1500
+ return apiClient.configureMcpPlans(productRef, params);
1501
+ },
1385
1502
  getVirtualTools(options) {
1386
1503
  return createVirtualTools(apiClient, options);
1387
1504
  },
1505
+ async registerVirtualToolsMcp(server, options) {
1506
+ await registerVirtualToolsMcpImpl(server, apiClient, options);
1507
+ },
1388
1508
  // Payable API for framework-specific handlers
1389
1509
  payable(options = {}) {
1390
1510
  const product = options.productRef || options.product || process.env.SOLVAPAY_PRODUCT || "default-product";
@@ -1423,9 +1543,9 @@ function createSolvaPay(config) {
1423
1543
  getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
1424
1544
  });
1425
1545
  const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
1426
- return async (args) => {
1546
+ return async (args, extra) => {
1427
1547
  const handler = await handlerPromise;
1428
- return handler(args);
1548
+ return handler(args, extra);
1429
1549
  };
1430
1550
  },
1431
1551
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1495,6 +1615,167 @@ function getCustomerRefFromBearerAuthHeader(authorization, options = {}) {
1495
1615
  return getCustomerRefFromJwtPayload(payload, options);
1496
1616
  }
1497
1617
 
1618
+ // src/mcp/auth-bridge.ts
1619
+ function getClientId(payload, explicitClientId) {
1620
+ if (explicitClientId) return explicitClientId;
1621
+ const payloadClientId = typeof payload.client_id === "string" && payload.client_id || typeof payload.azp === "string" && payload.azp || typeof payload.aud === "string" && payload.aud || null;
1622
+ return payloadClientId || "solvapay-mcp-client";
1623
+ }
1624
+ function getScopes(payload, defaultScopes) {
1625
+ if (Array.isArray(payload.scp)) {
1626
+ return payload.scp.filter((scope) => typeof scope === "string");
1627
+ }
1628
+ if (typeof payload.scope === "string" && payload.scope.trim()) {
1629
+ return payload.scope.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
1630
+ }
1631
+ return defaultScopes;
1632
+ }
1633
+ function getExpiresAt(payload) {
1634
+ return typeof payload.exp === "number" ? payload.exp : void 0;
1635
+ }
1636
+ function buildAuthInfoFromBearer(authorization, options = {}) {
1637
+ const token = extractBearerToken(authorization);
1638
+ if (!token) return null;
1639
+ const payload = decodeJwtPayload(token);
1640
+ const customerRef = getCustomerRefFromJwtPayload(payload, options);
1641
+ const clientId = getClientId(payload, options.clientId);
1642
+ const scopes = getScopes(payload, options.defaultScopes || []);
1643
+ const expiresAt = getExpiresAt(payload);
1644
+ return {
1645
+ token,
1646
+ clientId,
1647
+ scopes,
1648
+ expiresAt,
1649
+ extra: {
1650
+ customer_ref: customerRef,
1651
+ ...options.includePayload ? { payload } : {}
1652
+ }
1653
+ };
1654
+ }
1655
+
1656
+ // src/mcp/oauth-bridge.ts
1657
+ function withoutTrailingSlash(value) {
1658
+ return value.replace(/\/$/, "");
1659
+ }
1660
+ function getRequestAuthHeader(req) {
1661
+ const header = req.headers?.authorization;
1662
+ if (typeof header === "string") return header;
1663
+ if (Array.isArray(header)) return header[0] || null;
1664
+ return null;
1665
+ }
1666
+ function getRequestJsonRpcId(body) {
1667
+ if (body && typeof body === "object" && "id" in body) {
1668
+ const id = body.id;
1669
+ return id ?? null;
1670
+ }
1671
+ return null;
1672
+ }
1673
+ function makeUnauthorizedJsonRpc(id) {
1674
+ return {
1675
+ jsonrpc: "2.0",
1676
+ id,
1677
+ error: {
1678
+ code: -32001,
1679
+ message: "Unauthorized"
1680
+ }
1681
+ };
1682
+ }
1683
+ function setMcpChallengeHeader(res, publicBaseUrl, protectedResourcePath) {
1684
+ res.setHeader(
1685
+ "WWW-Authenticate",
1686
+ `Bearer resource_metadata="${withoutTrailingSlash(publicBaseUrl)}${protectedResourcePath}"`
1687
+ );
1688
+ }
1689
+ function getOAuthProtectedResourceResponse(publicBaseUrl) {
1690
+ const resource = withoutTrailingSlash(publicBaseUrl);
1691
+ return {
1692
+ resource,
1693
+ authorization_servers: [resource],
1694
+ scopes_supported: ["openid", "profile", "email"]
1695
+ };
1696
+ }
1697
+ function getOAuthAuthorizationServerResponse({
1698
+ apiBaseUrl,
1699
+ productRef
1700
+ }) {
1701
+ const normalizedApiBaseUrl = withoutTrailingSlash(apiBaseUrl);
1702
+ const registrationEndpoint = `${normalizedApiBaseUrl}/v1/customer/auth/register?product_ref=${encodeURIComponent(productRef)}`;
1703
+ return {
1704
+ issuer: normalizedApiBaseUrl,
1705
+ authorization_endpoint: `${normalizedApiBaseUrl}/v1/customer/auth/authorize`,
1706
+ token_endpoint: `${normalizedApiBaseUrl}/v1/customer/auth/token`,
1707
+ registration_endpoint: registrationEndpoint,
1708
+ token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
1709
+ response_types_supported: ["code"],
1710
+ grant_types_supported: ["authorization_code", "refresh_token"],
1711
+ scopes_supported: ["openid", "profile", "email"],
1712
+ code_challenge_methods_supported: ["S256"]
1713
+ };
1714
+ }
1715
+ function createMcpOAuthBridge(options) {
1716
+ const {
1717
+ publicBaseUrl,
1718
+ apiBaseUrl,
1719
+ productRef,
1720
+ mcpPath = "/mcp",
1721
+ requireAuth = true,
1722
+ authInfo,
1723
+ protectedResourcePath = "/.well-known/oauth-protected-resource",
1724
+ authorizationServerPath = "/.well-known/oauth-authorization-server"
1725
+ } = options;
1726
+ const protectedResourceMiddleware = (req, res, next) => {
1727
+ if (req.method !== "GET" || req.path !== protectedResourcePath) {
1728
+ next();
1729
+ return;
1730
+ }
1731
+ res.json(getOAuthProtectedResourceResponse(publicBaseUrl));
1732
+ };
1733
+ const authorizationServerMiddleware = (req, res, next) => {
1734
+ if (req.method !== "GET" || req.path !== authorizationServerPath) {
1735
+ next();
1736
+ return;
1737
+ }
1738
+ if (!productRef) {
1739
+ res.status(500).json({ error: "SOLVAPAY_PRODUCT_REF missing" });
1740
+ return;
1741
+ }
1742
+ res.json(
1743
+ getOAuthAuthorizationServerResponse({
1744
+ apiBaseUrl,
1745
+ productRef
1746
+ })
1747
+ );
1748
+ };
1749
+ const mcpAuthMiddleware = (req, res, next) => {
1750
+ if (req.path !== mcpPath) {
1751
+ next();
1752
+ return;
1753
+ }
1754
+ const authHeader = getRequestAuthHeader(req);
1755
+ const id = getRequestJsonRpcId(req.body);
1756
+ if (!authHeader && !requireAuth) {
1757
+ next();
1758
+ return;
1759
+ }
1760
+ try {
1761
+ const auth = buildAuthInfoFromBearer(authHeader, authInfo);
1762
+ if (!auth) {
1763
+ throw new McpBearerAuthError("Missing bearer token");
1764
+ }
1765
+ req.auth = auth;
1766
+ next();
1767
+ } catch {
1768
+ setMcpChallengeHeader(res, publicBaseUrl, protectedResourcePath);
1769
+ if (req.method === "POST") {
1770
+ res.status(401).json(makeUnauthorizedJsonRpc(id));
1771
+ return;
1772
+ }
1773
+ res.status(401).json({ error: "Unauthorized" });
1774
+ }
1775
+ };
1776
+ return [protectedResourceMiddleware, authorizationServerMiddleware, mcpAuthMiddleware];
1777
+ }
1778
+
1498
1779
  // src/helpers/error.ts
1499
1780
  import { SolvaPayError as SolvaPayError3 } from "@solvapay/core";
1500
1781
  function isErrorResult(result) {
@@ -1848,9 +2129,11 @@ export {
1848
2129
  McpBearerAuthError,
1849
2130
  PaywallError,
1850
2131
  VIRTUAL_TOOL_DEFINITIONS,
2132
+ buildAuthInfoFromBearer,
1851
2133
  cancelPurchaseCore,
1852
2134
  createCheckoutSessionCore,
1853
2135
  createCustomerSessionCore,
2136
+ createMcpOAuthBridge,
1854
2137
  createPaymentIntentCore,
1855
2138
  createSolvaPay,
1856
2139
  createSolvaPayClient,
@@ -1860,10 +2143,14 @@ export {
1860
2143
  getAuthenticatedUserCore,
1861
2144
  getCustomerRefFromBearerAuthHeader,
1862
2145
  getCustomerRefFromJwtPayload,
2146
+ getOAuthAuthorizationServerResponse,
2147
+ getOAuthProtectedResourceResponse,
1863
2148
  handleRouteError,
1864
2149
  isErrorResult,
2150
+ jsonSchemaToZodRawShape,
1865
2151
  listPlansCore,
1866
2152
  processPaymentIntentCore,
2153
+ registerVirtualToolsMcpImpl,
1867
2154
  syncCustomerCore,
1868
2155
  verifyWebhook,
1869
2156
  withRetry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solvapay/server",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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.2"
40
+ "@solvapay/core": "1.0.4"
41
41
  },
42
42
  "peerDependencies": {
43
- "@solvapay/auth": "1.0.2"
43
+ "@modelcontextprotocol/sdk": "^1.28.0",
44
+ "zod": "^3.25.0 || ^4.0.0",
45
+ "@solvapay/auth": "1.0.4"
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",
@@ -50,9 +60,9 @@
50
60
  "tsx": "^4.21.0",
51
61
  "typescript": "^5.9.3",
52
62
  "vitest": "^4.1.2",
53
- "@solvapay/auth": "1.0.2",
63
+ "@solvapay/demo-services": "0.0.0",
54
64
  "@solvapay/test-utils": "^0.0.0",
55
- "@solvapay/demo-services": "0.0.0"
65
+ "@solvapay/auth": "1.0.4"
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",