@oobe-protocol-labs/sap-mcp-server 0.3.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/.env.example +3 -0
  2. package/CHANGELOG.md +218 -2
  3. package/README.md +63 -0
  4. package/USER_DOCS/00_START_HERE.md +23 -1
  5. package/USER_DOCS/01_HOSTED_REMOTE_MCP.md +4 -1
  6. package/USER_DOCS/03_PAYMENTS_X402_PAYSH.md +63 -7
  7. package/USER_DOCS/04_CLIENT_CONFIGS.md +134 -6
  8. package/USER_DOCS/06_DESKTOP_GUI_WIZARD.md +262 -0
  9. package/dist/adapters/mcp/sdk-compat.d.ts.map +1 -1
  10. package/dist/adapters/mcp/sdk-compat.js +32 -18
  11. package/dist/adapters/mcp/sdk-compat.js.map +1 -1
  12. package/dist/config/mcp-client-injection.d.ts +64 -2
  13. package/dist/config/mcp-client-injection.d.ts.map +1 -1
  14. package/dist/config/mcp-client-injection.js +668 -11
  15. package/dist/config/mcp-client-injection.js.map +1 -1
  16. package/dist/config/wizard.d.ts.map +1 -1
  17. package/dist/config/wizard.js +89 -5
  18. package/dist/config/wizard.js.map +1 -1
  19. package/dist/core/constants.d.ts +1 -1
  20. package/dist/core/constants.js +1 -1
  21. package/dist/payments/facilitator-rpc-fallback.d.ts +43 -0
  22. package/dist/payments/facilitator-rpc-fallback.d.ts.map +1 -0
  23. package/dist/payments/facilitator-rpc-fallback.js +228 -0
  24. package/dist/payments/facilitator-rpc-fallback.js.map +1 -0
  25. package/dist/payments/json-rpc.d.ts.map +1 -1
  26. package/dist/payments/json-rpc.js +2 -1
  27. package/dist/payments/json-rpc.js.map +1 -1
  28. package/dist/payments/monetization-gate.d.ts +43 -2
  29. package/dist/payments/monetization-gate.d.ts.map +1 -1
  30. package/dist/payments/monetization-gate.js +618 -9
  31. package/dist/payments/monetization-gate.js.map +1 -1
  32. package/dist/payments/oobe-facilitator-server.d.ts +6 -0
  33. package/dist/payments/oobe-facilitator-server.d.ts.map +1 -1
  34. package/dist/payments/oobe-facilitator-server.js +79 -4
  35. package/dist/payments/oobe-facilitator-server.js.map +1 -1
  36. package/dist/payments/pricing.d.ts.map +1 -1
  37. package/dist/payments/pricing.js +1 -0
  38. package/dist/payments/pricing.js.map +1 -1
  39. package/dist/payments/usage-ledger.d.ts +10 -0
  40. package/dist/payments/usage-ledger.d.ts.map +1 -1
  41. package/dist/payments/usage-ledger.js +51 -0
  42. package/dist/payments/usage-ledger.js.map +1 -1
  43. package/dist/payments/x402-paid-call.d.ts +53 -0
  44. package/dist/payments/x402-paid-call.d.ts.map +1 -0
  45. package/dist/payments/x402-paid-call.js +370 -0
  46. package/dist/payments/x402-paid-call.js.map +1 -0
  47. package/dist/prompts/context/sap-agent-context.prompt.js +9 -3
  48. package/dist/prompts/context/sap-agent-context.prompt.js.map +1 -1
  49. package/dist/prompts/developer/debug-sap-error.prompt.js +1 -1
  50. package/dist/prompts/developer/debug-sap-error.prompt.js.map +1 -1
  51. package/dist/prompts/developer/generate-sap-integration.prompt.js +6 -2
  52. package/dist/prompts/developer/generate-sap-integration.prompt.js.map +1 -1
  53. package/dist/prompts/execution-proof/explain-proof-of-execution.prompt.js +1 -1
  54. package/dist/prompts/execution-proof/explain-proof-of-execution.prompt.js.map +1 -1
  55. package/dist/prompts/execution-proof/verify-execution-proof.prompt.js +1 -1
  56. package/dist/prompts/execution-proof/verify-execution-proof.prompt.js.map +1 -1
  57. package/dist/prompts/payments/create-paid-api.prompt.js +1 -1
  58. package/dist/prompts/payments/create-paid-api.prompt.js.map +1 -1
  59. package/dist/prompts/payments/explain-x402-settlement.prompt.js +1 -1
  60. package/dist/prompts/payments/explain-x402-settlement.prompt.js.map +1 -1
  61. package/dist/prompts/register-prompts.js +2 -2
  62. package/dist/prompts/register-prompts.js.map +1 -1
  63. package/dist/prompts/registry/analyze-sap-agent.prompt.js +1 -1
  64. package/dist/prompts/registry/analyze-sap-agent.prompt.js.map +1 -1
  65. package/dist/prompts/registry/register-sap-agent.prompt.js +2 -2
  66. package/dist/prompts/registry/register-sap-agent.prompt.js.map +1 -1
  67. package/dist/remote/server.d.ts +132 -1
  68. package/dist/remote/server.d.ts.map +1 -1
  69. package/dist/remote/server.js +885 -49
  70. package/dist/remote/server.js.map +1 -1
  71. package/dist/resources/current/sap-current-config.resource.d.ts.map +1 -1
  72. package/dist/resources/current/sap-current-config.resource.js +11 -1
  73. package/dist/resources/current/sap-current-config.resource.js.map +1 -1
  74. package/dist/resources/execution-proof/sap-execution-record.resource.js +1 -1
  75. package/dist/resources/execution-proof/sap-execution-record.resource.js.map +1 -1
  76. package/dist/resources/memory/sap-memory.resource.js +1 -1
  77. package/dist/resources/memory/sap-memory.resource.js.map +1 -1
  78. package/dist/resources/profile/sap-active-profile.resource.js +1 -1
  79. package/dist/resources/profile/sap-active-profile.resource.js.map +1 -1
  80. package/dist/resources/register-resources.js +2 -2
  81. package/dist/resources/register-resources.js.map +1 -1
  82. package/dist/resources/registry/sap-agent.resource.js +1 -1
  83. package/dist/resources/registry/sap-agent.resource.js.map +1 -1
  84. package/dist/resources/registry/sap-global-registry.resource.js +1 -1
  85. package/dist/resources/registry/sap-global-registry.resource.js.map +1 -1
  86. package/dist/resources/reputation/sap-reputation.resource.js +1 -1
  87. package/dist/resources/reputation/sap-reputation.resource.js.map +1 -1
  88. package/dist/resources/stats/sap-network-stats.resource.js +1 -1
  89. package/dist/resources/stats/sap-network-stats.resource.js.map +1 -1
  90. package/dist/resources/tool-schema/sap-tool-schema.resource.js +1 -1
  91. package/dist/resources/tool-schema/sap-tool-schema.resource.js.map +1 -1
  92. package/dist/sap/sap-client-manager.js +5 -5
  93. package/dist/sap/sap-client-manager.js.map +1 -1
  94. package/dist/security/tool-permissions.d.ts.map +1 -1
  95. package/dist/security/tool-permissions.js +1 -0
  96. package/dist/security/tool-permissions.js.map +1 -1
  97. package/dist/server/create-server.js +5 -5
  98. package/dist/server/create-server.js.map +1 -1
  99. package/dist/server/register-capabilities.js +4 -4
  100. package/dist/server/register-capabilities.js.map +1 -1
  101. package/dist/signer/signer-resolver.js +1 -1
  102. package/dist/signer/signer-resolver.js.map +1 -1
  103. package/dist/tools/client-sdk-tools.js +7 -7
  104. package/dist/tools/client-sdk-tools.js.map +1 -1
  105. package/dist/tools/index.d.ts +2 -0
  106. package/dist/tools/index.d.ts.map +1 -1
  107. package/dist/tools/index.js +4 -0
  108. package/dist/tools/index.js.map +1 -1
  109. package/dist/tools/magicblock-tools.d.ts +28 -0
  110. package/dist/tools/magicblock-tools.d.ts.map +1 -0
  111. package/dist/tools/magicblock-tools.js +578 -0
  112. package/dist/tools/magicblock-tools.js.map +1 -0
  113. package/dist/tools/profile-tools.d.ts.map +1 -1
  114. package/dist/tools/profile-tools.js +20 -0
  115. package/dist/tools/profile-tools.js.map +1 -1
  116. package/dist/tools/register-tools.d.ts.map +1 -1
  117. package/dist/tools/register-tools.js +11 -2
  118. package/dist/tools/register-tools.js.map +1 -1
  119. package/dist/tools/sap-sdk-tools.js +2 -2
  120. package/dist/tools/sap-sdk-tools.js.map +1 -1
  121. package/dist/tools/sap-sns-tools.js +2 -2
  122. package/dist/tools/sap-sns-tools.js.map +1 -1
  123. package/dist/tools/skills-tools.d.ts +1 -1
  124. package/dist/tools/skills-tools.d.ts.map +1 -1
  125. package/dist/tools/skills-tools.js +10 -1
  126. package/dist/tools/skills-tools.js.map +1 -1
  127. package/dist/tools/tool-aliases.d.ts +10 -0
  128. package/dist/tools/tool-aliases.d.ts.map +1 -0
  129. package/dist/tools/tool-aliases.js +23 -0
  130. package/dist/tools/tool-aliases.js.map +1 -0
  131. package/dist/tools/x402-paid-call-tool.d.ts +12 -0
  132. package/dist/tools/x402-paid-call-tool.d.ts.map +1 -0
  133. package/dist/tools/x402-paid-call-tool.js +118 -0
  134. package/dist/tools/x402-paid-call-tool.js.map +1 -0
  135. package/dist/wizard-core/desktop-flow.d.ts +82 -0
  136. package/dist/wizard-core/desktop-flow.d.ts.map +1 -0
  137. package/dist/wizard-core/desktop-flow.js +181 -0
  138. package/dist/wizard-core/desktop-flow.js.map +1 -0
  139. package/docs/00_README.md +3 -0
  140. package/docs/03_CONFIGURATION_AND_WIZARD.md +31 -5
  141. package/docs/06_PAYMENTS_X402_AND_PAYSH.md +70 -7
  142. package/docs/07_ENDPOINTS_AND_CLIENTS.md +28 -1
  143. package/docs/10_OPERATIONS_RELEASE_AND_PM2.md +7 -6
  144. package/docs/13_BOUNTY_PROGRAM_PROPOSAL.md +469 -0
  145. package/docs/14_DESKTOP_WIZARD_RELEASE.md +264 -0
  146. package/docs/README.md +104 -0
  147. package/docs/_sidebar.md +31 -0
  148. package/docs/magicblock-tools.md +166 -0
  149. package/docs/x402-protocol-spec.md +984 -0
  150. package/package.json +23 -2
  151. package/server.json +71 -0
  152. package/skills/sap-defi/SKILL.md +8 -0
  153. package/skills/sap-mcp/SKILL.md +30 -0
  154. package/skills/sap-payments-x402/SKILL.md +36 -4
@@ -6,13 +6,16 @@ import { HTTPFacilitatorClient, x402HTTPResourceServer, x402ResourceServer } fro
6
6
  import { registerExactSvmScheme } from '@x402/svm/exact/server';
7
7
  import { logger } from '../core/logger.js';
8
8
  import { NativeHttpAdapter, parseJsonBody, readRequestBody } from './http-adapter.js';
9
- import { parseJsonRpcBody } from './json-rpc.js';
9
+ import { isRecord, parseJsonRpcBody } from './json-rpc.js';
10
10
  import { formatUsdPrice, resolvePaymentDecision } from './pricing.js';
11
- import { hashRequestBody, UsageLedger } from './usage-ledger.js';
11
+ import { hashPaymentRequest, UsageLedger } from './usage-ledger.js';
12
+ import { isTransientRpcError } from './facilitator-rpc-fallback.js';
12
13
  const SOLANA_MAINNET_CAIP2 = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
13
14
  const SOLANA_DEVNET_CAIP2 = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1';
14
15
  const SOLANA_TESTNET_CAIP2 = 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z';
15
16
  const PAID_ROUTE_PATTERN = 'POST /mcp/paid/*';
17
+ const USDC_MAINNET = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
18
+ const USDC_DEVNET = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU';
16
19
  /**
17
20
  * @name McpMonetizationGate
18
21
  * @description Applies x402 payment verification and settlement before forwarding requests to the MCP transport.
@@ -70,7 +73,7 @@ export class McpMonetizationGate {
70
73
  * @name handle
71
74
  * @description Authorizes a request via x402 when required, then forwards it to MCP and settles on success.
72
75
  */
73
- async handle(request, response, next) {
76
+ async handle(request, response, next, options = {}) {
74
77
  if (!shouldInspectRequest(request)) {
75
78
  await next(request, response);
76
79
  return;
@@ -82,20 +85,81 @@ export class McpMonetizationGate {
82
85
  parsedBody = parseJsonBody(rawBody);
83
86
  }
84
87
  catch (error) {
88
+ // Non-JSON or empty body (e.g. x402scan probing, GET requests, health checks).
89
+ // Return a 402 payment challenge with the standard x402 body so discovery
90
+ // scanners can detect the paywall.
91
+ if (shouldInspectRequest(request)) {
92
+ await this.returnX402Challenge(request, response, { jsonrpc: '2.0', method: 'tools/list', params: {} });
93
+ return;
94
+ }
85
95
  logger.warn('Failed to parse monetized MCP request', { error });
86
96
  writeJson(response, 400, { error: 'Invalid JSON-RPC request body' });
87
97
  return;
88
98
  }
89
99
  const parsedMcp = parseJsonRpcBody(parsedBody);
90
100
  const decision = resolvePaymentDecision(parsedMcp, this.monetization);
101
+ // For non-MCP clients (x402 scanners, curl without Accept header), always
102
+ // return 402 — the paywall must fire before body validation per the x402
103
+ // discovery spec: "Registration probes must reach a 402 challenge before
104
+ // body/query validation rejects the request."
105
+ // MCP SDK uniquely sends Accept: application/json, text/event-stream.
106
+ // x402scan sends Accept: */* or Accept: application/json (without text/event-stream).
107
+ // Only text/event-stream is a reliable MCP SDK marker.
108
+ const acceptHeader = request.headers['accept'] || '';
109
+ const isMcpSdkClient = acceptHeader.includes('text/event-stream');
91
110
  if (!decision.required) {
111
+ if (shouldInspectRequest(request) && !isMcpSdkClient) {
112
+ // x402 scanner probing with arbitrary body — return 402 challenge
113
+ await this.returnX402Challenge(request, response, parsedBody);
114
+ return;
115
+ }
92
116
  await next(request, response, parsedBody);
93
117
  return;
94
118
  }
95
- const requestHash = hashRequestBody(rawBody);
119
+ const requiredDecision = decision;
120
+ const requestHash = hashPaymentRequest(rawBody, parsedBody);
96
121
  const virtualPath = buildPaidVirtualPath(decision, requestHash);
122
+ // Standard x402 scanners and non-MCP HTTP clients must see a normal HTTP
123
+ // 402 challenge before MCP transport/session validation runs. MCP SDK
124
+ // clients are the only callers that need the JSON-RPC compatible 200 error.
125
+ const paymentHeader = request.headers['payment-signature'] ??
126
+ request.headers['x-payment'];
127
+ if (!paymentHeader && !isMcpSdkClient) {
128
+ await this.returnX402Challenge(request, response, parsedBody, virtualPath);
129
+ return;
130
+ }
131
+ const validationFailure = options.validatePaidRequest?.({
132
+ request,
133
+ parsedBody,
134
+ decision: requiredDecision,
135
+ });
136
+ if (validationFailure) {
137
+ writeJsonRpcError(response, parsedBody, validationFailure.code, validationFailure.message, validationFailure.data);
138
+ return;
139
+ }
97
140
  const metadata = buildRequestMetadata(request, requestHash, virtualPath);
98
141
  await this.usageLedger.recordDecision(metadata, decision);
142
+ logger.info('Payment required', {
143
+ toolNames: decision.toolNames,
144
+ price: decision.price,
145
+ callerIp: metadata.remoteAddress,
146
+ requestHash: requestHash.slice(0, 12),
147
+ });
148
+ // ── Fast path: Payment-Signature header present ────────────────────────
149
+ if (paymentHeader) {
150
+ await this.handleSignedPayment({
151
+ request,
152
+ response,
153
+ next,
154
+ parsedBody,
155
+ decision: requiredDecision,
156
+ metadata,
157
+ paymentHeader,
158
+ virtualPath,
159
+ });
160
+ return;
161
+ }
162
+ // ── Normal path: no Payment-Signature header, generate 402 response ──────
99
163
  const context = this.buildRequestContext(request, parsedBody, virtualPath);
100
164
  const paymentResult = await this.httpResourceServer.processHTTPRequest(context, {
101
165
  appName: 'SAP MCP Server',
@@ -103,7 +167,88 @@ export class McpMonetizationGate {
103
167
  testnet: resolvePaymentNetwork(this.appConfig) !== SOLANA_MAINNET_CAIP2,
104
168
  });
105
169
  if (paymentResult.type === 'payment-error') {
106
- writePaymentInstructions(response, paymentResult.response);
170
+ // V1 compatibility: @x402/svm V1 exact scheme checks maxAmountRequired but
171
+ // V2 resource server only emits amount. Patch the 402 response body to add
172
+ // maxAmountRequired so V1-only clients and facilitators can verify payments.
173
+ patchV1Compatibility(paymentResult.response);
174
+ // Extract the x402 PaymentRequired object from the PAYMENT-REQUIRED header.
175
+ // The x402 resource server puts the accepts[] (with extra.feePayer) in this
176
+ // header as base64-encoded JSON.
177
+ let acceptsFromHeader = undefined;
178
+ try {
179
+ const paymentRequiredHeader = paymentResult.response.headers?.['payment-required'] ??
180
+ paymentResult.response.headers?.['PAYMENT-REQUIRED'];
181
+ if (typeof paymentRequiredHeader === 'string') {
182
+ const decoded = Buffer.from(paymentRequiredHeader, 'base64').toString('utf-8');
183
+ const paymentRequired = JSON.parse(decoded);
184
+ acceptsFromHeader = paymentRequired.accepts;
185
+ }
186
+ }
187
+ catch {
188
+ // Header not present or invalid — fall back to body
189
+ }
190
+ // Determine if this is a genuine MCP SDK client or an x402 scanner.
191
+ // isMcpSdkClient was already determined above from the Accept header.
192
+ // MCP SDK sends Accept: application/json, text/event-stream.
193
+ // x402 scanners (x402scan.com) send standard HTTP requests without MCP Accept.
194
+ const responseBody = paymentResult.response.body;
195
+ const jsonRpcId = extractJsonRpcId(parsedBody);
196
+ // Use accepts from header (has extra.feePayer) or fall back to body
197
+ const paymentRequiredHeader = paymentResult.response.headers?.['payment-required'] ??
198
+ paymentResult.response.headers?.['PAYMENT-REQUIRED'];
199
+ if (typeof paymentRequiredHeader === 'string') {
200
+ response.setHeader('PAYMENT-REQUIRED', paymentRequiredHeader);
201
+ }
202
+ const paymentRequirements = acceptsFromHeader ??
203
+ responseBody?.accepts ??
204
+ responseBody?.paymentRequirements ??
205
+ responseBody;
206
+ if (!isMcpSdkClient) {
207
+ // x402 scanner or non-MCP HTTP client — return standard HTTP 402
208
+ // with the full PaymentRequired object (x402Version, accepts, resource)
209
+ // decoded from the PAYMENT-REQUIRED header as the response body.
210
+ const prHeaderDecoded = paymentRequiredHeader;
211
+ let x402Body = {
212
+ error: 'Payment required',
213
+ accepts: paymentRequirements,
214
+ };
215
+ if (typeof prHeaderDecoded === 'string') {
216
+ try {
217
+ x402Body = JSON.parse(Buffer.from(prHeaderDecoded, 'base64').toString('utf-8'));
218
+ }
219
+ catch {
220
+ // fall back to the simple body
221
+ }
222
+ }
223
+ response.writeHead(402, { 'Content-Type': 'application/json' });
224
+ response.end(JSON.stringify(x402Body));
225
+ return;
226
+ }
227
+ // MCP SDK client — return HTTP 200 with JSON-RPC error (the SDK hangs on 402)
228
+ writeJson(response, 200, {
229
+ jsonrpc: '2.0',
230
+ id: jsonRpcId,
231
+ error: {
232
+ code: -32001,
233
+ message: 'payment_required',
234
+ data: {
235
+ protocol: 'x402',
236
+ paymentRequirements,
237
+ instructions: {
238
+ header: 'Payment-Signature',
239
+ settlementHeader: 'Payment-Response',
240
+ requestHashBinding: 'json-rpc-method-and-params',
241
+ requiredRetryHeaders: ['Payment-Signature', 'mcp-session-id', 'mcp-protocol-version'],
242
+ paymentSignatureFormat: 'base64(JSON.stringify({ x402Version, accepted: paymentRequirements[0], payload, resource }))',
243
+ steps: [
244
+ 'Initialize the MCP session first and reuse the returned mcp-session-id for the unpaid call and paid retry.',
245
+ 'Build the payment from paymentRequirements[0] / accepts[0] returned by this response.',
246
+ 'Retry the same MCP method and params with the Payment-Signature header.',
247
+ ],
248
+ },
249
+ },
250
+ },
251
+ });
107
252
  return;
108
253
  }
109
254
  if (paymentResult.type === 'no-payment-required') {
@@ -111,6 +256,11 @@ export class McpMonetizationGate {
111
256
  return;
112
257
  }
113
258
  await this.usageLedger.recordVerified(metadata, decision);
259
+ logger.info('Payment verified', {
260
+ toolNames: decision.toolNames,
261
+ price: decision.price,
262
+ requestHash: metadata.requestHash.slice(0, 12),
263
+ });
114
264
  await this.forwardAndSettle({
115
265
  request,
116
266
  response,
@@ -122,7 +272,146 @@ export class McpMonetizationGate {
122
272
  parsedBody,
123
273
  });
124
274
  }
125
- buildRequestContext(request, parsedBody, virtualPath) {
275
+ /**
276
+ * @name handleSignedPayment
277
+ * @description Verifies a client-supplied x402 payment payload against the original accepted requirements.
278
+ */
279
+ async handleSignedPayment(options) {
280
+ let parsedPayment;
281
+ try {
282
+ parsedPayment = parsePaymentHeader(options.paymentHeader);
283
+ }
284
+ catch (error) {
285
+ logger.warn('Invalid Payment-Signature header', {
286
+ error,
287
+ requestHash: options.metadata.requestHash.slice(0, 12),
288
+ });
289
+ writeJsonRpcError(options.response, options.parsedBody, -32002, 'payment_verification_failed', {
290
+ reason: 'invalid_payment_signature_header',
291
+ hint: 'Payment-Signature must be base64(JSON) containing x402Version, accepted, and payload.',
292
+ });
293
+ return;
294
+ }
295
+ const validationError = validatePaymentRequirementsForDecision(parsedPayment.paymentRequirements, parsedPayment.paymentPayload, options.decision, this.appConfig, options.virtualPath);
296
+ if (validationError) {
297
+ logger.warn('Payment requirements rejected before facilitator verify', {
298
+ reason: validationError,
299
+ toolNames: options.decision.toolNames,
300
+ requestHash: options.metadata.requestHash.slice(0, 12),
301
+ });
302
+ writeJsonRpcError(options.response, options.parsedBody, -32002, 'payment_verification_failed', {
303
+ reason: validationError,
304
+ hint: 'Retry with Payment-Signature generated from this request paymentRequirements[0] / accepts[0]. Preserve the initialized MCP session id.',
305
+ });
306
+ return;
307
+ }
308
+ logger.info('Payment-Signature detected — direct facilitator verification', {
309
+ toolNames: options.decision.toolNames,
310
+ price: options.decision.price,
311
+ requestHash: options.metadata.requestHash.slice(0, 12),
312
+ });
313
+ let verifyResult;
314
+ try {
315
+ verifyResult = await this.callFacilitator('verify', {
316
+ x402Version: parsedPayment.x402Version,
317
+ paymentPayload: parsedPayment.paymentPayload,
318
+ paymentRequirements: parsedPayment.paymentRequirements,
319
+ });
320
+ }
321
+ catch (error) {
322
+ const failure = classifyFacilitatorError(error);
323
+ logger.warn('Payment verification facilitator call failed', {
324
+ reason: failure.reason,
325
+ message: failure.message,
326
+ retryable: failure.retryable,
327
+ requestHash: options.metadata.requestHash.slice(0, 12),
328
+ });
329
+ writeJsonRpcError(options.response, options.parsedBody, -32002, 'payment_verification_failed', failure);
330
+ return;
331
+ }
332
+ if (!verifyResult.isValid) {
333
+ const failure = classifyVerifyFailure(verifyResult);
334
+ logger.warn('Payment verification failed', {
335
+ reason: failure.reason,
336
+ message: failure.message,
337
+ retryable: failure.retryable,
338
+ category: failure.category,
339
+ payer: verifyResult.payer,
340
+ requestHash: options.metadata.requestHash.slice(0, 12),
341
+ });
342
+ writeJsonRpcError(options.response, options.parsedBody, -32002, 'payment_verification_failed', failure);
343
+ return;
344
+ }
345
+ await this.usageLedger.recordVerified(options.metadata, options.decision);
346
+ const buffered = bufferResponse(options.response);
347
+ try {
348
+ await options.next(options.request, options.response, options.parsedBody);
349
+ await buffered.waitForEnd();
350
+ }
351
+ catch (error) {
352
+ await this.usageLedger.recordCanceled(options.metadata, options.decision, 'handler_threw', error instanceof Error ? error.message : String(error));
353
+ buffered.restore();
354
+ throw error;
355
+ }
356
+ if (options.response.statusCode >= 400) {
357
+ await this.usageLedger.recordCanceled(options.metadata, options.decision, 'handler_failed', `MCP handler returned HTTP ${options.response.statusCode}`);
358
+ buffered.restoreAndReplay();
359
+ return;
360
+ }
361
+ let settlement;
362
+ try {
363
+ settlement = await this.callFacilitator('settle', {
364
+ x402Version: parsedPayment.x402Version,
365
+ paymentPayload: parsedPayment.paymentPayload,
366
+ paymentRequirements: parsedPayment.paymentRequirements,
367
+ });
368
+ }
369
+ catch (error) {
370
+ const failure = classifyFacilitatorError(error);
371
+ await this.usageLedger.recordCanceled(options.metadata, options.decision, 'handler_failed', failure.message ?? failure.reason ?? 'facilitator_settle_failed');
372
+ buffered.restore();
373
+ writeJsonRpcError(options.response, options.parsedBody, -32003, 'payment_settlement_failed', failure);
374
+ return;
375
+ }
376
+ await this.usageLedger.recordSettlement(options.metadata, options.decision, settlement);
377
+ if (!settlement.success) {
378
+ logger.warn('Settlement failed', {
379
+ toolNames: options.decision.toolNames,
380
+ price: options.decision.price,
381
+ requestHash: options.metadata.requestHash.slice(0, 12),
382
+ errorReason: settlement.errorReason,
383
+ });
384
+ buffered.restore();
385
+ writeJsonRpcError(options.response, options.parsedBody, -32003, 'payment_settlement_failed', {
386
+ reason: settlement.errorReason,
387
+ message: settlement.errorMessage,
388
+ });
389
+ return;
390
+ }
391
+ logger.info('Settlement success', {
392
+ toolNames: options.decision.toolNames,
393
+ price: options.decision.price,
394
+ tx: settlement.transaction,
395
+ amount: settlement.amount,
396
+ payer: settlement.payer,
397
+ requestHash: options.metadata.requestHash.slice(0, 12),
398
+ });
399
+ setPaymentResponseHeaders(options.response, settlement);
400
+ buffered.restoreAndReplay();
401
+ }
402
+ async callFacilitator(path, body) {
403
+ const response = await fetch(buildFacilitatorUrl(this.monetization, path), {
404
+ method: 'POST',
405
+ headers: buildFacilitatorHeaders(this.monetization),
406
+ body: JSON.stringify(body),
407
+ });
408
+ if (!response.ok) {
409
+ const bodyText = await response.text().catch(() => '');
410
+ throw new Error(`Facilitator ${path} failed with HTTP ${response.status}${bodyText ? `: ${bodyText}` : ''}`);
411
+ }
412
+ return await response.json();
413
+ }
414
+ buildRequestContext(request, parsedBody, virtualPath, method = request.method ?? 'POST') {
126
415
  const adapter = new NativeHttpAdapter({
127
416
  request,
128
417
  path: virtualPath,
@@ -131,10 +420,53 @@ export class McpMonetizationGate {
131
420
  return {
132
421
  adapter,
133
422
  path: virtualPath,
134
- method: request.method ?? 'POST',
423
+ method,
135
424
  paymentHeader: adapter.getHeader('payment-signature') ?? adapter.getHeader('x-payment'),
136
425
  };
137
426
  }
427
+ /**
428
+ * @name returnX402Challenge
429
+ * @description Returns a standard HTTP 402 payment challenge for non-MCP clients
430
+ * (x402scan discovery, curl, health checks). Decodes the PaymentRequired object
431
+ * from the PAYMENT-REQUIRED header and returns it as the response body so x402
432
+ * scanners can validate the challenge format.
433
+ */
434
+ async returnX402Challenge(request, response, parsedBody, virtualPath = '/mcp/paid/read-premium/x402scan-probe/tools/list') {
435
+ // Use a path that matches the PAID_ROUTE_PATTERN ('POST /mcp/paid/*')
436
+ // so the x402 resource server generates a payment challenge.
437
+ const context = this.buildRequestContext(request, parsedBody, virtualPath, 'POST');
438
+ const paymentResult = await this.httpResourceServer.processHTTPRequest(context, {
439
+ appName: 'SAP MCP Server',
440
+ currentUrl: context.adapter.getUrl(),
441
+ testnet: resolvePaymentNetwork(this.appConfig) !== SOLANA_MAINNET_CAIP2,
442
+ });
443
+ if (paymentResult.type !== 'payment-error') {
444
+ // No payment required for this route — fall through
445
+ response.writeHead(200, { 'Content-Type': 'application/json' });
446
+ response.end(JSON.stringify({ ok: true }));
447
+ return;
448
+ }
449
+ patchV1Compatibility(paymentResult.response);
450
+ // Set the PAYMENT-REQUIRED header from the x402 resource server
451
+ const prHeader = paymentResult.response.headers?.['payment-required'] ??
452
+ paymentResult.response.headers?.['PAYMENT-REQUIRED'];
453
+ if (typeof prHeader === 'string') {
454
+ response.setHeader('PAYMENT-REQUIRED', prHeader);
455
+ }
456
+ // Decode the PaymentRequired object from the header and return it as the body.
457
+ // x402 scanners expect: { x402Version, error, accepts, resource }
458
+ let x402Body = paymentResult.response.body ?? { error: 'Payment required' };
459
+ if (typeof prHeader === 'string') {
460
+ try {
461
+ x402Body = JSON.parse(Buffer.from(prHeader, 'base64').toString('utf-8'));
462
+ }
463
+ catch {
464
+ // fall back to the response body
465
+ }
466
+ }
467
+ response.writeHead(402, { 'Content-Type': 'application/json' });
468
+ response.end(JSON.stringify(x402Body));
469
+ }
138
470
  async forwardAndSettle(options) {
139
471
  const buffered = bufferResponse(options.response);
140
472
  try {
@@ -179,11 +511,26 @@ export class McpMonetizationGate {
179
511
  responseHeaders,
180
512
  });
181
513
  await this.usageLedger.recordSettlement(options.metadata, options.decision, settlement);
514
+ const decision = options.decision;
182
515
  if (!settlement.success) {
516
+ logger.warn('Settlement failed', {
517
+ toolNames: decision.toolNames,
518
+ price: decision.price,
519
+ requestHash: options.metadata.requestHash.slice(0, 12),
520
+ errorReason: settlement.errorReason,
521
+ });
183
522
  options.buffered.restore();
184
523
  writePaymentInstructions(options.response, settlement.response);
185
524
  return;
186
525
  }
526
+ logger.info('Settlement success', {
527
+ toolNames: decision.toolNames,
528
+ price: decision.price,
529
+ tx: settlement.transaction,
530
+ amount: settlement.amount,
531
+ payer: settlement.payer,
532
+ requestHash: options.metadata.requestHash.slice(0, 12),
533
+ });
187
534
  for (const [name, value] of Object.entries(settlement.headers)) {
188
535
  options.response.setHeader(name, value);
189
536
  }
@@ -286,16 +633,216 @@ function buildPayShCheckoutUrl(config, decision) {
286
633
  return url.toString();
287
634
  }
288
635
  function shouldInspectRequest(request) {
289
- return request.method === 'POST' && Boolean(request.url?.startsWith('/mcp'));
636
+ // Accept both GET and POST on /mcp so x402 discovery scanners (x402scan.com)
637
+ // that probe with GET requests trigger the payment challenge.
638
+ return Boolean(request.url?.startsWith('/mcp'));
290
639
  }
291
640
  /**
292
641
  * @name buildPaidVirtualPath
293
- * @description Builds the paid x402 virtual resource path, bound to the exact JSON-RPC request hash.
642
+ * @description Builds the paid x402 virtual resource path, bound to the canonical MCP method-and-params hash.
294
643
  */
295
644
  export function buildPaidVirtualPath(decision, requestHash) {
296
645
  const toolSegment = decision.toolNames.map(name => encodeURIComponent(name)).join(',');
297
646
  return `/mcp/paid/${decision.tier}/${requestHash}/${toolSegment}`;
298
647
  }
648
+ function parsePaymentHeader(header) {
649
+ const parsed = parsePaymentHeaderJson(header);
650
+ if (!isRecord(parsed)) {
651
+ throw new Error('Payment-Signature must decode to a JSON object.');
652
+ }
653
+ const accepted = parsePaymentRequirements(parsed.accepted);
654
+ const paymentPayload = {
655
+ x402Version: typeof parsed.x402Version === 'number' ? parsed.x402Version : 2,
656
+ accepted,
657
+ payload: isRecord(parsed.payload) ? parsed.payload : {},
658
+ ...(isRecord(parsed.resource) ? { resource: parseResourceInfo(parsed.resource) } : {}),
659
+ ...(isRecord(parsed.extensions) ? { extensions: parsed.extensions } : {}),
660
+ };
661
+ return {
662
+ x402Version: paymentPayload.x402Version,
663
+ paymentPayload,
664
+ paymentRequirements: accepted,
665
+ };
666
+ }
667
+ function parsePaymentHeaderJson(header) {
668
+ const candidates = [
669
+ Buffer.from(header, 'base64').toString('utf-8'),
670
+ header,
671
+ ];
672
+ for (const candidate of candidates) {
673
+ try {
674
+ return JSON.parse(candidate);
675
+ }
676
+ catch {
677
+ // Try the next representation. Some legacy clients sent raw JSON.
678
+ }
679
+ }
680
+ throw new Error('Invalid Payment-Signature header.');
681
+ }
682
+ function parsePaymentRequirements(value) {
683
+ if (!isRecord(value)) {
684
+ throw new Error('Payment-Signature payload is missing accepted payment requirements.');
685
+ }
686
+ const scheme = requireString(value, 'scheme');
687
+ const network = requireString(value, 'network');
688
+ const asset = requireString(value, 'asset');
689
+ const amount = requireString(value, 'amount');
690
+ const payTo = requireString(value, 'payTo');
691
+ const maxTimeoutSeconds = requireNumber(value, 'maxTimeoutSeconds');
692
+ const extra = isRecord(value.extra) ? value.extra : {};
693
+ const maxAmountRequired = typeof value.maxAmountRequired === 'string'
694
+ ? value.maxAmountRequired
695
+ : amount;
696
+ return {
697
+ scheme,
698
+ network,
699
+ asset,
700
+ amount,
701
+ maxAmountRequired,
702
+ payTo,
703
+ maxTimeoutSeconds,
704
+ extra,
705
+ };
706
+ }
707
+ function parseResourceInfo(value) {
708
+ const resource = {
709
+ url: requireString(value, 'url'),
710
+ };
711
+ if (typeof value.description === 'string')
712
+ resource.description = value.description;
713
+ if (typeof value.mimeType === 'string')
714
+ resource.mimeType = value.mimeType;
715
+ if (typeof value.serviceName === 'string')
716
+ resource.serviceName = value.serviceName;
717
+ if (Array.isArray(value.tags) && value.tags.every(tag => typeof tag === 'string')) {
718
+ resource.tags = value.tags;
719
+ }
720
+ if (typeof value.iconUrl === 'string')
721
+ resource.iconUrl = value.iconUrl;
722
+ return resource;
723
+ }
724
+ function validatePaymentRequirementsForDecision(requirements, payload, decision, config, virtualPath) {
725
+ const network = resolvePaymentNetwork(config);
726
+ const expectedAmount = usdToUsdcBaseUnits(decision.priceUsd);
727
+ const expectedAsset = getUsdcAsset(network);
728
+ const expectedPayTo = config.monetization.payTo;
729
+ if (requirements.scheme !== 'exact')
730
+ return 'unsupported_scheme';
731
+ if (requirements.network !== network)
732
+ return 'network_mismatch';
733
+ if (requirements.payTo !== expectedPayTo)
734
+ return 'pay_to_mismatch';
735
+ if (requirements.amount !== expectedAmount)
736
+ return 'amount_mismatch';
737
+ if (requirements.maxAmountRequired && requirements.maxAmountRequired !== expectedAmount) {
738
+ return 'max_amount_required_mismatch';
739
+ }
740
+ if (requirements.asset !== expectedAsset)
741
+ return 'asset_mismatch';
742
+ if (!isRecord(requirements.extra) || typeof requirements.extra.feePayer !== 'string') {
743
+ return 'missing_fee_payer';
744
+ }
745
+ if (payload.accepted.amount !== requirements.amount || payload.accepted.payTo !== requirements.payTo) {
746
+ return 'payload_accepted_mismatch';
747
+ }
748
+ if (payload.resource?.url && !payload.resource.url.includes(virtualPath)) {
749
+ return 'resource_mismatch';
750
+ }
751
+ return undefined;
752
+ }
753
+ function usdToUsdcBaseUnits(priceUsd) {
754
+ return Math.round(priceUsd * 1_000_000).toString();
755
+ }
756
+ function getUsdcAsset(network) {
757
+ return network === SOLANA_MAINNET_CAIP2 ? USDC_MAINNET : USDC_DEVNET;
758
+ }
759
+ function requireString(record, key) {
760
+ const value = record[key];
761
+ if (typeof value !== 'string' || value.length === 0) {
762
+ throw new Error(`Missing required string field: ${key}`);
763
+ }
764
+ return value;
765
+ }
766
+ function requireNumber(record, key) {
767
+ const value = record[key];
768
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
769
+ throw new Error(`Missing required number field: ${key}`);
770
+ }
771
+ return value;
772
+ }
773
+ function buildFacilitatorUrl(config, path) {
774
+ const baseUrl = config.facilitatorUrl;
775
+ if (!baseUrl) {
776
+ throw new Error('SAP MCP monetization requires SAP_MCP_X402_FACILITATOR_URL');
777
+ }
778
+ return `${baseUrl.replace(/\/+$/, '')}/${path}`;
779
+ }
780
+ function buildFacilitatorHeaders(config) {
781
+ return {
782
+ 'Content-Type': 'application/json',
783
+ ...(config.facilitatorAuthToken ? { Authorization: `Bearer ${config.facilitatorAuthToken}` } : {}),
784
+ };
785
+ }
786
+ function setPaymentResponseHeaders(response, settlement) {
787
+ const encoded = Buffer.from(JSON.stringify(settlement), 'utf-8').toString('base64');
788
+ response.setHeader('PAYMENT-RESPONSE', encoded);
789
+ response.setHeader('X-PAYMENT-RESPONSE', encoded);
790
+ }
791
+ function classifyVerifyFailure(verifyResult) {
792
+ const reason = verifyResult.invalidReason;
793
+ const message = verifyResult.invalidMessage;
794
+ const combined = [reason, message].filter(Boolean).join(': ');
795
+ const retryable = isTransientFacilitatorFailure(combined);
796
+ if (retryable) {
797
+ return {
798
+ reason,
799
+ message,
800
+ retryable: true,
801
+ category: 'facilitator_rpc',
802
+ hint: 'The payment could not be simulated because the facilitator or its Solana RPC returned a transient gateway/availability error. Retry the same paid call; if it repeats, check facilitator health, configured RPC fallbacks, and the facilitator PM2 logs.',
803
+ };
804
+ }
805
+ return {
806
+ reason,
807
+ message,
808
+ retryable: false,
809
+ category: 'client_payment',
810
+ hint: 'Check USDC balance, network, payTo, feePayer, token account setup, and that the payment was built from this exact request challenge.',
811
+ };
812
+ }
813
+ function classifyFacilitatorError(error) {
814
+ const message = error instanceof Error ? error.message : String(error);
815
+ const retryable = isTransientFacilitatorFailure(message);
816
+ return {
817
+ reason: retryable ? 'facilitator_unavailable' : 'facilitator_request_failed',
818
+ message,
819
+ retryable,
820
+ category: retryable ? 'facilitator_unavailable' : 'client_payment',
821
+ hint: retryable
822
+ ? 'The facilitator or one of its RPC endpoints is temporarily unavailable. Retry the paid call; if it repeats, verify facilitator /health, /supported, auth token, and RPC fallback configuration.'
823
+ : 'The facilitator rejected the verification or settlement request. Check the payment payload, accepted requirements, facilitator URL, and facilitator auth token.',
824
+ };
825
+ }
826
+ function isTransientFacilitatorFailure(message) {
827
+ if (!message) {
828
+ return false;
829
+ }
830
+ const normalized = message.toLowerCase();
831
+ return isTransientRpcError(message)
832
+ || containsAny(normalized, [
833
+ 'transaction_simulation_failed',
834
+ 'smart_wallet_simulation_failed',
835
+ 'facilitator verify failed with http 502',
836
+ 'facilitator verify failed with http 503',
837
+ 'facilitator verify failed with http 504',
838
+ 'facilitator settle failed with http 502',
839
+ 'facilitator settle failed with http 503',
840
+ 'facilitator settle failed with http 504',
841
+ ]);
842
+ }
843
+ function containsAny(message, needles) {
844
+ return needles.some(needle => message.includes(needle));
845
+ }
299
846
  function buildRequestMetadata(request, requestHash, virtualPath) {
300
847
  const paymentHeader = request.headers['payment-signature'] ?? request.headers['x-payment'];
301
848
  return {
@@ -322,6 +869,17 @@ function writeJson(response, status, body) {
322
869
  response.writeHead(status, { 'Content-Type': 'application/json' });
323
870
  response.end(JSON.stringify(body));
324
871
  }
872
+ function writeJsonRpcError(response, requestBody, code, message, data) {
873
+ writeJson(response, 200, {
874
+ jsonrpc: '2.0',
875
+ id: extractJsonRpcId(requestBody),
876
+ error: {
877
+ code,
878
+ message,
879
+ ...(data === undefined ? {} : { data }),
880
+ },
881
+ });
882
+ }
325
883
  function getResponseHeaders(response) {
326
884
  const headers = {};
327
885
  for (const [name, value] of Object.entries(response.getHeaders())) {
@@ -412,4 +970,55 @@ function replayBufferedCall(call, writeHead, write, end, flushHeaders) {
412
970
  return;
413
971
  }
414
972
  }
973
+ /**
974
+ * @name extractJsonRpcId
975
+ * @description Extracts the JSON-RPC id from a parsed request body for response correlation.
976
+ */
977
+ function extractJsonRpcId(body) {
978
+ if (!body || typeof body !== 'object')
979
+ return null;
980
+ const record = body;
981
+ if (typeof record.id === 'string' || typeof record.id === 'number')
982
+ return record.id;
983
+ if (Array.isArray(body) && body.length > 0) {
984
+ return extractJsonRpcId(body[0]);
985
+ }
986
+ return null;
987
+ }
988
+ /**
989
+ * @name patchV1Compatibility
990
+ * @description Patches the x402 402 response body to add maxAmountRequired (V1) alongside amount (V2).
991
+ * The @x402/svm V1 exact scheme verifier checks maxAmountRequired, but the V2 resource server
992
+ * only emits amount. This ensures V1-compatible clients and facilitators can verify payments.
993
+ */
994
+ function patchV1Compatibility(instructions) {
995
+ if (instructions.isHtml || !instructions.body) {
996
+ return;
997
+ }
998
+ const body = instructions.body;
999
+ if (!body || typeof body !== 'object') {
1000
+ return;
1001
+ }
1002
+ // x402 response body: { accepts: [{ amount, ... }, ...], ... }
1003
+ const accepts = body.accepts;
1004
+ if (Array.isArray(accepts)) {
1005
+ for (const req of accepts) {
1006
+ if (req && typeof req === 'object' && 'amount' in req && !('maxAmountRequired' in req)) {
1007
+ req.maxAmountRequired = req.amount;
1008
+ }
1009
+ }
1010
+ }
1011
+ // Also patch top-level paymentRequirements if present
1012
+ const requirements = body.paymentRequirements;
1013
+ if (Array.isArray(requirements)) {
1014
+ for (const req of requirements) {
1015
+ if (req && typeof req === 'object' && 'amount' in req && !('maxAmountRequired' in req)) {
1016
+ req.maxAmountRequired = req.amount;
1017
+ }
1018
+ }
1019
+ }
1020
+ else if (requirements && typeof requirements === 'object' && 'amount' in requirements && !('maxAmountRequired' in requirements)) {
1021
+ requirements.maxAmountRequired = requirements.amount;
1022
+ }
1023
+ }
415
1024
  //# sourceMappingURL=monetization-gate.js.map