@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,7 +6,7 @@
6
6
  import { randomUUID } from 'crypto';
7
7
  import { existsSync, readFileSync } from 'fs';
8
8
  import * as http from 'http';
9
- import { dirname, join } from 'path';
9
+ import { dirname, join, posix } from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
12
12
  import { getDataDir, loadConfig } from '../config/env.js';
@@ -14,12 +14,22 @@ import { MCP_SERVER_VERSION } from '../core/constants.js';
14
14
  import { logger, initLogger } from '../core/logger.js';
15
15
  import { createSapMcpServer } from '../server/create-server.js';
16
16
  import { AuthManager } from './auth/index.js';
17
- import { McpMonetizationGate } from '../payments/index.js';
17
+ import { McpMonetizationGate, resolvePaymentNetwork } from '../payments/index.js';
18
+ import { generatePayShProviderYaml } from '../payments/pay-sh-spec.js';
18
19
  import { RemoteRateLimiter, buildRemoteRateLimitConfigFromEnv } from './rate-limiter.js';
19
20
  const PUBLIC_SERVER_TITLE = 'SAP MCP Server | OOBE Protocol';
20
21
  const PUBLIC_SERVER_DESCRIPTION = 'Hosted Solana-native MCP gateway for Synapse Agent Protocol tools, x402/pay.sh monetization, SNS identity, and agent operations.';
21
22
  const LOGO_ASSET_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'assets', 'explorer_logo.png');
23
+ const DOCS_ROOT_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'docs');
24
+ const USER_DOCS_ROOT_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'USER_DOCS');
22
25
  const PAYMENT_STATS_CACHE_MS = 15_000;
26
+ const RELEASE_DOWNLOAD_BASE_URL = `https://github.com/OOBE-PROTOCOL/sap-mcp/releases/download/${MCP_SERVER_VERSION}`;
27
+ const WIZARD_NPM_COMMAND = 'npm exec --yes --package @oobe-protocol-labs/sap-mcp-server -- sap-mcp-config wizard';
28
+ const X402_PAID_CALL_NPX_COMMAND = 'npx --yes --package @oobe-protocol-labs/sap-mcp-server sap-mcp-x402-paid-call --tool sap_list_all_agents --arguments \'{"limit":5}\' --max-usd 0.02 --confirm';
29
+ const X402_PAID_CALL_ADDON_PATH = '~/.config/mcp-sap/addons/x402-paid-call';
30
+ const SOLANA_DEVNET_CAIP2 = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1';
31
+ const USDC_MAINNET_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
32
+ const USDC_DEVNET_MINT = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU';
23
33
  let logoAssetCache;
24
34
  let paymentStatsCache;
25
35
  /**
@@ -53,8 +63,13 @@ export function defaultRemoteConfig(appConfig) {
53
63
  port,
54
64
  host,
55
65
  corsOrigins: appConfig.httpCorsOrigins,
56
- stateless: parseRemoteBoolean(process.env.SAP_MCP_HTTP_STATELESS, appConfig.mode === 'hosted-api'),
66
+ stateless: parseRemoteBoolean(process.env.SAP_MCP_HTTP_STATELESS, false),
57
67
  rateLimit: buildRemoteRateLimitConfigFromEnv(appConfig.rateLimitPerMinute),
68
+ paymentDiscovery: buildPublicPaymentDiscovery(appConfig),
69
+ paymentPriceRange: {
70
+ minUsd: appConfig.monetization.prices.minUsd,
71
+ maxUsd: appConfig.monetization.prices.maxUsd,
72
+ },
58
73
  };
59
74
  if (authType === 'none') {
60
75
  return {
@@ -86,6 +101,56 @@ export function defaultRemoteConfig(appConfig) {
86
101
  },
87
102
  };
88
103
  }
104
+ /**
105
+ * @name buildPublicPaymentDiscovery
106
+ * @description Builds public x402 seller/facilitator coordinates without exposing facilitator URL or auth tokens.
107
+ */
108
+ function buildPublicPaymentDiscovery(appConfig) {
109
+ if (!appConfig.monetization.enabled || !appConfig.monetization.payTo) {
110
+ return undefined;
111
+ }
112
+ const network = resolvePaymentNetwork(appConfig);
113
+ const facilitatorPublicKey = (process.env.SAP_MCP_X402_FACILITATOR_PUBLIC_KEY
114
+ ?? process.env.SAP_MCP_X402_FACILITATOR_FEE_PAYER
115
+ ?? '').trim();
116
+ return {
117
+ provider: appConfig.monetization.provider,
118
+ network,
119
+ asset: resolvePublicPaymentAsset(network),
120
+ assetSymbol: 'USDC',
121
+ payTo: appConfig.monetization.payTo,
122
+ seller: {
123
+ name: 'OOBE Protocol',
124
+ payTo: appConfig.monetization.payTo,
125
+ },
126
+ ...(facilitatorPublicKey
127
+ ? {
128
+ facilitator: {
129
+ role: 'x402-svm-fee-payer',
130
+ publicKey: facilitatorPublicKey,
131
+ },
132
+ }
133
+ : {}),
134
+ headers: {
135
+ paymentRequired: 'PAYMENT-REQUIRED',
136
+ paymentSignature: 'PAYMENT-SIGNATURE',
137
+ paymentResponse: 'PAYMENT-RESPONSE',
138
+ },
139
+ };
140
+ }
141
+ /**
142
+ * @name resolvePublicPaymentAsset
143
+ * @description Resolves the public USDC mint advertised in x402 discovery metadata.
144
+ */
145
+ function resolvePublicPaymentAsset(network) {
146
+ if (network === SOLANA_DEVNET_CAIP2) {
147
+ return USDC_DEVNET_MINT;
148
+ }
149
+ return USDC_MAINNET_MINT;
150
+ }
151
+ function formatOpenApiUsd(value) {
152
+ return value.toFixed(6).replace(/0+$/, '').replace(/\.$/, '');
153
+ }
89
154
  function parseRemoteBoolean(raw, fallback) {
90
155
  if (raw === undefined) {
91
156
  return fallback;
@@ -96,38 +161,208 @@ function parseRemoteBoolean(raw, fallback) {
96
161
  * @name writeJson
97
162
  * @description Writes a JSON response with optional additional headers.
98
163
  */
99
- function writeJson(res, status, body, headers = {}) {
164
+ function writeJson(res, status, body, headers = {}, omitBody = false) {
165
+ const serialized = JSON.stringify(body);
100
166
  res.writeHead(status, {
101
167
  'Content-Type': 'application/json',
168
+ 'Content-Length': Buffer.byteLength(serialized),
102
169
  ...headers,
103
170
  });
104
- res.end(JSON.stringify(body));
171
+ res.end(omitBody ? undefined : serialized);
172
+ }
173
+ /**
174
+ * @name writeText
175
+ * @description Writes a cacheable text response with an explicit content type.
176
+ */
177
+ function writeText(res, status, body, contentType, headers = {}, omitBody = false) {
178
+ res.writeHead(status, {
179
+ 'Content-Type': contentType,
180
+ 'Cache-Control': 'public, max-age=300',
181
+ 'Content-Length': Buffer.byteLength(body),
182
+ 'X-Content-Type-Options': 'nosniff',
183
+ ...headers,
184
+ });
185
+ res.end(omitBody ? undefined : body);
186
+ }
187
+ /**
188
+ * @name writeMcpJsonRpcError
189
+ * @description Writes an MCP-compatible JSON-RPC error response with HTTP status semantics.
190
+ */
191
+ function writeMcpJsonRpcError(res, status, code, id, message, data) {
192
+ writeJson(res, status, {
193
+ jsonrpc: '2.0',
194
+ id,
195
+ error: {
196
+ code,
197
+ message,
198
+ ...(data === undefined ? {} : { data }),
199
+ },
200
+ });
201
+ }
202
+ /**
203
+ * @name readSingleHeader
204
+ * @description Reads one normalized HTTP header value without accepting ambiguous arrays.
205
+ */
206
+ function readSingleHeader(value) {
207
+ if (Array.isArray(value)) {
208
+ return value[0]?.trim() || undefined;
209
+ }
210
+ return value?.trim() || undefined;
211
+ }
212
+ /**
213
+ * @name extractJsonRpcId
214
+ * @description Extracts a JSON-RPC id for error correlation.
215
+ */
216
+ function extractJsonRpcId(body) {
217
+ if (Array.isArray(body) && body.length > 0) {
218
+ return extractJsonRpcId(body[0]);
219
+ }
220
+ if (!body || typeof body !== 'object') {
221
+ return null;
222
+ }
223
+ const record = body;
224
+ return typeof record.id === 'string' || typeof record.id === 'number' ? record.id : null;
225
+ }
226
+ /**
227
+ * @name isInitializeJsonRpcRequest
228
+ * @description Detects MCP initialize requests that are allowed to create a new stateful session.
229
+ */
230
+ function isInitializeJsonRpcRequest(body) {
231
+ if (Array.isArray(body)) {
232
+ return body.some(isInitializeJsonRpcRequest);
233
+ }
234
+ if (!body || typeof body !== 'object') {
235
+ return false;
236
+ }
237
+ return body.method === 'initialize';
105
238
  }
106
239
  /**
107
240
  * @name writeHtml
108
241
  * @description Writes a cacheable HTML response for public endpoint previews and social sharing.
109
242
  */
110
- function writeHtml(res, status, html) {
243
+ function writeHtml(res, status, html, omitBody = false) {
111
244
  res.writeHead(status, {
112
245
  'Content-Type': 'text/html; charset=utf-8',
113
246
  'Cache-Control': 'public, max-age=300',
247
+ 'Content-Length': Buffer.byteLength(html),
248
+ 'X-Content-Type-Options': 'nosniff',
249
+ });
250
+ res.end(omitBody ? undefined : html);
251
+ }
252
+ /**
253
+ * @name isPublicReadMethod
254
+ * @description Returns true for cacheable public metadata reads, including crawler-friendly HEAD checks.
255
+ */
256
+ function isPublicReadMethod(method) {
257
+ return method === 'GET' || method === 'HEAD';
258
+ }
259
+ /**
260
+ * @name isHeadMethod
261
+ * @description Returns true when the request must emit headers without a response body.
262
+ */
263
+ function isHeadMethod(method) {
264
+ return method === 'HEAD';
265
+ }
266
+ /**
267
+ * @name writeMarkdownAsset
268
+ * @description Writes a resolved public markdown documentation asset.
269
+ */
270
+ function writeMarkdownAsset(res, asset) {
271
+ res.writeHead(200, {
272
+ 'Content-Type': asset.contentType,
273
+ 'Cache-Control': 'public, max-age=120',
274
+ 'Content-Length': asset.contentLength,
114
275
  'X-Content-Type-Options': 'nosniff',
115
276
  });
116
- res.end(html);
277
+ res.end(asset.body);
117
278
  }
118
279
  /**
119
- * @name writePngAsset
120
- * @description Writes the bundled SAP logo as a public icon/social image asset.
280
+ * @name resolvePublicDocsMarkdown
281
+ * @description Resolves safe hosted documentation markdown requests under /docs and /docs/user.
121
282
  */
122
- function writePngAsset(res) {
283
+ export function resolvePublicDocsMarkdown(method, pathname) {
284
+ if (method !== 'GET' && method !== 'HEAD') {
285
+ return undefined;
286
+ }
287
+ if (!pathname.startsWith('/docs/') || !pathname.endsWith('.md')) {
288
+ return undefined;
289
+ }
290
+ let decodedPath;
291
+ try {
292
+ decodedPath = decodeURIComponent(pathname.slice('/docs/'.length));
293
+ }
294
+ catch {
295
+ return undefined;
296
+ }
297
+ const normalizedPath = posix.normalize(decodedPath.replace(/\\/g, '/')).replace(/^[/\\]+/, '');
298
+ if (isUnsafeDocsPath(normalizedPath)) {
299
+ return undefined;
300
+ }
301
+ const root = normalizedPath.startsWith('user/')
302
+ ? USER_DOCS_ROOT_PATH
303
+ : DOCS_ROOT_PATH;
304
+ const relativePath = normalizedPath.startsWith('user/')
305
+ ? normalizedPath.slice('user/'.length)
306
+ : normalizedPath;
307
+ if (isUnsafeDocsPath(relativePath)) {
308
+ return undefined;
309
+ }
310
+ const absolutePath = join(root, relativePath);
311
+ if (!existsSync(absolutePath)) {
312
+ return undefined;
313
+ }
314
+ const markdown = readFileSync(absolutePath, 'utf-8');
315
+ return {
316
+ contentType: 'text/markdown; charset=utf-8',
317
+ contentLength: Buffer.byteLength(markdown),
318
+ body: method === 'HEAD' ? undefined : markdown,
319
+ };
320
+ }
321
+ function isUnsafeDocsPath(pathname) {
322
+ return pathname === ''
323
+ || pathname.includes('\0')
324
+ || pathname === '..'
325
+ || pathname.startsWith('../')
326
+ || pathname.includes('/../');
327
+ }
328
+ /**
329
+ * @name resolvePublicLogoAsset
330
+ * @description Resolves public icon/social image routes, including HEAD-safe favicon responses for crawlers.
331
+ */
332
+ export function resolvePublicLogoAsset(method, pathname) {
333
+ if (method !== 'GET' && method !== 'HEAD') {
334
+ return undefined;
335
+ }
123
336
  const image = readLogoAsset();
337
+ const body = method === 'HEAD' ? undefined : image;
338
+ if (['/favicon.png', '/apple-touch-icon.png', '/og.png'].includes(pathname)) {
339
+ return {
340
+ contentType: 'image/png',
341
+ contentLength: image.byteLength,
342
+ body,
343
+ };
344
+ }
345
+ if (pathname === '/favicon.ico') {
346
+ return {
347
+ contentType: 'image/x-icon',
348
+ contentLength: image.byteLength,
349
+ body,
350
+ };
351
+ }
352
+ return undefined;
353
+ }
354
+ /**
355
+ * @name writeLogoAsset
356
+ * @description Writes a resolved public logo asset response.
357
+ */
358
+ function writeLogoAsset(res, asset) {
124
359
  res.writeHead(200, {
125
- 'Content-Type': 'image/png',
360
+ 'Content-Type': asset.contentType,
126
361
  'Cache-Control': 'public, max-age=86400',
127
- 'Content-Length': image.byteLength,
362
+ 'Content-Length': asset.contentLength,
128
363
  'X-Content-Type-Options': 'nosniff',
129
364
  });
130
- res.end(image);
365
+ res.end(asset.body);
131
366
  }
132
367
  /**
133
368
  * @name readLogoAsset
@@ -265,13 +500,27 @@ export function buildPublicServerInfo(req, config) {
265
500
  },
266
501
  endpoints: {
267
502
  landing: `${baseUrl}/`,
503
+ docs: `${baseUrl}/docs`,
268
504
  mcp: `${baseUrl}/mcp`,
269
505
  health: `${baseUrl}/health`,
270
506
  serverInfo: `${baseUrl}/server.json`,
507
+ openApi: `${baseUrl}/openapi.json`,
508
+ x402Discovery: `${baseUrl}/.well-known/x402`,
509
+ payShProvider: `${baseUrl}/pay/provider.yml`,
271
510
  agentCard: `${baseUrl}/.well-known/agent-card.json`,
272
511
  wizardDescriptor: `${baseUrl}/.well-known/sap-mcp-wizard.json`,
512
+ wizardDownloads: `${baseUrl}/wizard/downloads.json`,
273
513
  wizardInstallScript: `${baseUrl}/wizard/install.sh`,
274
514
  favicon: `${baseUrl}/favicon.png`,
515
+ faviconIco: `${baseUrl}/favicon.ico`,
516
+ },
517
+ downloads: {
518
+ release: `https://github.com/OOBE-PROTOCOL/sap-mcp/releases/tag/${MCP_SERVER_VERSION}`,
519
+ desktopWizard: {
520
+ macosArm64Dmg: `${RELEASE_DOWNLOAD_BASE_URL}/SAP-MCP-Wizard-${MCP_SERVER_VERSION}-arm64.dmg`,
521
+ windowsX64Setup: `${RELEASE_DOWNLOAD_BASE_URL}/SAP-MCP-Wizard-Setup-${MCP_SERVER_VERSION}-x64.exe`,
522
+ linuxX64TarGz: `${RELEASE_DOWNLOAD_BASE_URL}/sap-mcp-wizard-${MCP_SERVER_VERSION}-x64.tar.gz`,
523
+ },
275
524
  },
276
525
  capabilities: {
277
526
  tools: true,
@@ -285,6 +534,7 @@ export function buildPublicServerInfo(req, config) {
285
534
  schemes: authRequired ? ['Bearer'] : ['none', 'x402'],
286
535
  bearerRequired: authRequired,
287
536
  },
537
+ ...(config.paymentDiscovery ? { payments: config.paymentDiscovery } : {}),
288
538
  security: {
289
539
  keypairBytesExposed: false,
290
540
  storesUserKeypairs: false,
@@ -298,6 +548,237 @@ export function buildPublicServerInfo(req, config) {
298
548
  },
299
549
  };
300
550
  }
551
+ /**
552
+ * @name buildPublicPayShProviderYaml
553
+ * @description Builds a public pay.sh provider catalog for the hosted SAP MCP endpoint without RPC secrets or signer paths.
554
+ */
555
+ export function buildPublicPayShProviderYaml(req, config, appConfig) {
556
+ const baseUrl = buildPublicBaseUrl(req, config);
557
+ return generatePayShProviderYaml(appConfig, {
558
+ name: 'oobe-sap-mcp',
559
+ subdomain: 'oobe-sap-mcp',
560
+ title: 'OOBE SAP MCP Server',
561
+ description: PUBLIC_SERVER_DESCRIPTION,
562
+ version: MCP_SERVER_VERSION,
563
+ upstreamUrl: baseUrl,
564
+ recipient: config.paymentDiscovery?.payTo ?? appConfig.monetization.payTo,
565
+ rpcUrl: undefined,
566
+ signerPath: undefined,
567
+ });
568
+ }
569
+ /**
570
+ * @name buildOpenApiSpec
571
+ * @description OpenAPI 3.1 spec for x402scan discovery. Describes the /mcp endpoint
572
+ * with x-payment-info so x402scan can discover and register the API.
573
+ */
574
+ export function buildOpenApiSpec(req, config) {
575
+ const baseUrl = buildPublicBaseUrl(req, config);
576
+ const priceRange = config.paymentPriceRange ?? { minUsd: 0.001, maxUsd: 100 };
577
+ return {
578
+ openapi: '3.1.0',
579
+ info: {
580
+ title: 'SAP MCP Server',
581
+ version: MCP_SERVER_VERSION,
582
+ description: 'MCP gateway for Solana DeFi, SAP agent registry, and x402 micropayments. 248 tools across Jupiter, Drift, Metaplex, Pump.fun, Raydium, Orca, and more.',
583
+ 'x-guidance': 'Connect to POST /mcp with Accept: application/json, text/event-stream. Initialize an MCP session first, then call tools. Paid tools return a payment_required JSON-RPC error (HTTP 200 for MCP SDK compatibility) or HTTP 402 for non-MCP clients. Use x402_paid_call or npx sap-mcp-x402-paid-call to self-pay and retry.',
584
+ contact: {
585
+ name: 'OOBE Protocol Labs',
586
+ url: 'https://github.com/OOBE-PROTOCOL/sap-mcp',
587
+ email: 'oobe@oobeprotocol.ai',
588
+ },
589
+ },
590
+ 'x-discovery': {
591
+ resources: [`${baseUrl}/mcp`],
592
+ openApi: `${baseUrl}/openapi.json`,
593
+ x402Discovery: `${baseUrl}/.well-known/x402`,
594
+ payShProvider: `${baseUrl}/pay/provider.yml`,
595
+ ...(config.paymentDiscovery ? { payments: config.paymentDiscovery } : {}),
596
+ },
597
+ 'x-pay-sh': {
598
+ providerYaml: `${baseUrl}/pay/provider.yml`,
599
+ routing: {
600
+ type: 'proxy',
601
+ upstream: `${baseUrl}/`,
602
+ },
603
+ monetization: {
604
+ mode: 'metered',
605
+ unit: 'requests',
606
+ sourceOfTruth: 'SAP MCP per-tool x402 pricing remains authoritative after proxy forwarding.',
607
+ },
608
+ },
609
+ servers: [
610
+ { url: baseUrl, description: 'SAP MCP Hosted Server' },
611
+ ],
612
+ paths: {
613
+ '/mcp': {
614
+ post: {
615
+ operationId: 'mcpCall',
616
+ summary: 'Fetch SAP MCP JSON-RPC response',
617
+ tags: ['MCP'],
618
+ 'x-payment-info': {
619
+ price: {
620
+ mode: 'dynamic',
621
+ currency: 'USD',
622
+ min: formatOpenApiUsd(priceRange.minUsd),
623
+ max: formatOpenApiUsd(priceRange.maxUsd),
624
+ },
625
+ protocols: ['x402'],
626
+ ...(config.paymentDiscovery
627
+ ? {
628
+ network: config.paymentDiscovery.network,
629
+ asset: config.paymentDiscovery.asset,
630
+ assetSymbol: config.paymentDiscovery.assetSymbol,
631
+ payTo: config.paymentDiscovery.payTo,
632
+ seller: config.paymentDiscovery.seller,
633
+ facilitator: config.paymentDiscovery.facilitator,
634
+ }
635
+ : {}),
636
+ },
637
+ requestBody: {
638
+ required: true,
639
+ content: {
640
+ 'application/json': {
641
+ schema: {
642
+ oneOf: [
643
+ {
644
+ type: 'object',
645
+ description: 'General MCP JSON-RPC request such as initialize, tools/list, prompts/list, resources/list, or ping.',
646
+ properties: {
647
+ jsonrpc: { type: 'string', const: '2.0' },
648
+ id: { oneOf: [{ type: 'integer' }, { type: 'string' }], description: 'JSON-RPC request ID' },
649
+ method: {
650
+ type: 'string',
651
+ description: 'MCP method.',
652
+ examples: ['initialize', 'tools/list', 'prompts/list', 'resources/list', 'ping'],
653
+ },
654
+ params: { type: 'object', additionalProperties: true },
655
+ },
656
+ required: ['jsonrpc', 'id', 'method'],
657
+ },
658
+ {
659
+ type: 'object',
660
+ description: 'MCP tools/call request. Paid tool calls may require x402 payment before execution.',
661
+ properties: {
662
+ jsonrpc: { type: 'string', const: '2.0' },
663
+ id: { oneOf: [{ type: 'integer' }, { type: 'string' }], description: 'JSON-RPC request ID' },
664
+ method: { type: 'string', const: 'tools/call' },
665
+ params: {
666
+ type: 'object',
667
+ description: 'MCP tools/call parameters containing the target tool name and optional tool arguments.',
668
+ properties: {
669
+ name: { type: 'string', description: 'Tool name (e.g. jupiter_getPrice)' },
670
+ arguments: { type: 'object', description: 'Tool arguments', additionalProperties: true },
671
+ },
672
+ required: ['name'],
673
+ additionalProperties: true,
674
+ },
675
+ },
676
+ required: ['jsonrpc', 'id', 'method', 'params'],
677
+ },
678
+ ],
679
+ },
680
+ },
681
+ },
682
+ },
683
+ responses: {
684
+ '200': {
685
+ description: 'MCP tool result or payment_required JSON-RPC error (for MCP SDK clients)',
686
+ content: {
687
+ 'application/json': {
688
+ schema: {
689
+ type: 'object',
690
+ properties: {
691
+ jsonrpc: { type: 'string' },
692
+ id: { type: 'integer' },
693
+ result: { type: 'object', description: 'Tool result (if free or paid)' },
694
+ error: {
695
+ type: 'object',
696
+ properties: {
697
+ code: { type: 'integer' },
698
+ message: { type: 'string' },
699
+ data: {
700
+ type: 'object',
701
+ properties: {
702
+ protocol: { type: 'string' },
703
+ paymentRequirements: { type: 'array' },
704
+ },
705
+ },
706
+ },
707
+ },
708
+ },
709
+ },
710
+ },
711
+ 'text/event-stream': {
712
+ schema: {
713
+ type: 'string',
714
+ description: 'Streamable HTTP MCP event stream containing JSON-RPC response events.',
715
+ },
716
+ },
717
+ },
718
+ },
719
+ '402': {
720
+ description: 'Payment Required (x402 challenge for non-MCP clients)',
721
+ content: {
722
+ 'application/json': {
723
+ schema: {
724
+ type: 'object',
725
+ properties: {
726
+ x402Version: { type: 'integer' },
727
+ error: { type: 'string' },
728
+ accepts: {
729
+ type: 'array',
730
+ items: {
731
+ type: 'object',
732
+ properties: {
733
+ scheme: { type: 'string' },
734
+ network: { type: 'string' },
735
+ amount: { type: 'string', description: 'USDC atomic units (6 decimals)' },
736
+ asset: { type: 'string' },
737
+ payTo: { type: 'string' },
738
+ maxTimeoutSeconds: { type: 'integer' },
739
+ extra: {
740
+ type: 'object',
741
+ properties: {
742
+ feePayer: { type: 'string' },
743
+ },
744
+ },
745
+ },
746
+ },
747
+ },
748
+ resource: {
749
+ type: 'object',
750
+ properties: {
751
+ url: { type: 'string' },
752
+ description: { type: 'string' },
753
+ mimeType: { type: 'string' },
754
+ serviceName: { type: 'string' },
755
+ tags: { type: 'array', items: { type: 'string' } },
756
+ },
757
+ },
758
+ },
759
+ },
760
+ },
761
+ },
762
+ },
763
+ },
764
+ },
765
+ },
766
+ },
767
+ };
768
+ }
769
+ /**
770
+ * @name buildX402DiscoveryDocument
771
+ * @description Builds the compatibility discovery document consumed by x402scan.
772
+ */
773
+ export function buildX402DiscoveryDocument(req, config) {
774
+ const baseUrl = buildPublicBaseUrl(req, config);
775
+ return {
776
+ version: 1,
777
+ resources: [`${baseUrl}/mcp`],
778
+ ...(config.paymentDiscovery ? { payments: config.paymentDiscovery } : {}),
779
+ instructions: 'SAP MCP exposes paid hosted MCP tool calls at /mcp. Probe POST /mcp without a payment header to receive a parseable x402 challenge for paid tools.',
780
+ };
781
+ }
301
782
  /**
302
783
  * @name buildLandingHtml
303
784
  * @description Builds professional public HTML metadata for root and `/mcp` URL previews.
@@ -310,8 +791,9 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
310
791
  ? 'SAP MCP Streamable HTTP Endpoint | OOBE Protocol'
311
792
  : info.title;
312
793
  const endpointLabel = endpoint === 'mcp' ? 'Streamable HTTP MCP endpoint' : 'Hosted MCP gateway';
313
- const wizardCommand = 'npm exec --yes --package @oobe-protocol-labs/sap-mcp-server -- sap-mcp-config wizard';
794
+ const wizardCommand = WIZARD_NPM_COMMAND;
314
795
  const installScriptCommand = 'curl -fsSL https://mcp.sap.oobeprotocol.ai/wizard/install.sh | sh';
796
+ const x402PaidCallCommand = X402_PAID_CALL_NPX_COMMAND;
315
797
  const publicInfo = JSON.stringify(info, null, 2).replace(/</g, '\\u003c');
316
798
  return `<!doctype html>
317
799
  <html lang="en">
@@ -322,8 +804,9 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
322
804
  <meta name="description" content="${escapeHtml(info.description)}">
323
805
  <meta name="robots" content="index,follow">
324
806
  <link rel="canonical" href="${escapeHtml(pageUrl)}">
807
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
325
808
  <link rel="icon" type="image/png" href="/favicon.png">
326
- <link rel="shortcut icon" type="image/png" href="/favicon.png">
809
+ <link rel="shortcut icon" href="/favicon.ico">
327
810
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
328
811
  <meta property="og:type" content="website">
329
812
  <meta property="og:title" content="${escapeHtml(title)}">
@@ -428,6 +911,21 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
428
911
  .list { padding-left: 18px; margin: 0; color: var(--muted); }
429
912
  .list li + li { margin-top: 8px; }
430
913
  .command { margin-top: 12px; border: 1px solid rgba(255,255,255,.10); border-radius: 8px; background: rgba(0,0,0,.28); padding: 13px; }
914
+ .download-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 14px; }
915
+ .download-link {
916
+ display: grid;
917
+ min-height: 74px;
918
+ align-content: center;
919
+ gap: 4px;
920
+ border: 1px solid var(--line);
921
+ border-radius: 8px;
922
+ background: rgba(0,0,0,.18);
923
+ padding: 12px;
924
+ color: var(--ink);
925
+ text-decoration: none;
926
+ }
927
+ .download-link strong { color: var(--aqua); }
928
+ .download-link span { color: var(--muted); font-size: .88rem; }
431
929
  .endpoint-list { display: grid; gap: 10px; margin-top: 10px; }
432
930
  .endpoint-row { display: grid; grid-template-columns: 72px 1fr; gap: 12px; align-items: baseline; }
433
931
  .method { color: var(--success); font: 700 .78rem/1 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
@@ -436,6 +934,7 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
436
934
  header { grid-template-columns: 1fr; }
437
935
  img.logo { width: 76px; height: 76px; border-radius: 20px; }
438
936
  .span-3, .span-4, .span-5, .span-6, .span-7, .span-8 { grid-column: span 12; }
937
+ .download-grid { grid-template-columns: 1fr; }
439
938
  }
440
939
  @media (min-width: 861px) and (max-width: 1120px) {
441
940
  .span-3, .span-4 { grid-column: span 6; }
@@ -456,7 +955,11 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
456
955
  <p>${escapeHtml(info.description)}</p>
457
956
  <div class="hero-actions">
458
957
  <a class="button" href="/wizard/install.sh">Install wizard</a>
958
+ <a class="button secondary" href="/wizard/downloads.json">Downloads</a>
959
+ <a class="button secondary" href="/docs">Docs</a>
459
960
  <a class="button secondary" href="/server.json">Server JSON</a>
961
+ <a class="button secondary" href="/openapi.json">OpenAPI</a>
962
+ <a class="button secondary" href="/pay/provider.yml">pay.sh YAML</a>
460
963
  <a class="button secondary" href="/.well-known/agent-card.json">Agent card</a>
461
964
  </div>
462
965
  </div>
@@ -488,21 +991,43 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
488
991
  <div class="command"><code>${escapeHtml(wizardCommand)}</code></div>
489
992
  </section>
490
993
 
994
+ <section class="card span-5">
995
+ <h2>Native Downloads</h2>
996
+ <p>Use the desktop wizard when you want a guided setup without touching config files by hand.</p>
997
+ <div class="download-grid">
998
+ <a class="download-link" href="${escapeHtml(info.downloads.desktopWizard.windowsX64Setup)}"><strong>Windows</strong><span>x64 setup .exe</span></a>
999
+ <a class="download-link" href="${escapeHtml(info.downloads.desktopWizard.macosArm64Dmg)}"><strong>macOS</strong><span>Apple Silicon .dmg</span></a>
1000
+ <a class="download-link" href="${escapeHtml(info.downloads.desktopWizard.linuxX64TarGz)}"><strong>Linux</strong><span>x64 tar.gz</span></a>
1001
+ </div>
1002
+ <p class="caption">Machine-readable links: <a href="${escapeHtml(info.endpoints.wizardDownloads)}">/wizard/downloads.json</a>.</p>
1003
+ </section>
1004
+
491
1005
  <section class="card span-5">
492
1006
  <h2>Payments</h2>
493
1007
  <ul class="list">
494
1008
  <li><strong>x402:</strong> paid MCP tool calls return HTTP 402 with payment requirements, then settle through the OOBE facilitator.</li>
495
- <li><strong>pay.sh:</strong> supports pay-per-use and subscription specs for agents or dashboards that prefer checkout flows.</li>
1009
+ <li><strong>pay.sh:</strong> public provider YAML is available at <a href="${escapeHtml(info.endpoints.payShProvider)}">/pay/provider.yml</a> for catalog and proxy workflows.</li>
496
1010
  <li><strong>Local stdio:</strong> free by default; monetization applies to hosted remote usage.</li>
497
1011
  </ul>
498
1012
  </section>
499
1013
 
1014
+ <section class="card strong span-12">
1015
+ <h2>x402 Paid-Call Plugin For Agents</h2>
1016
+ <p>Agents that can reach hosted MCP but cannot replay x402 challenges natively can install the wizard addon or invoke the local paid-call helper directly. The helper signs the payment payload locally and retries the hosted tool call without sending keypair bytes to OOBE.</p>
1017
+ <div class="command"><code>Addon path: ${escapeHtml(X402_PAID_CALL_ADDON_PATH)}</code></div>
1018
+ <div class="command"><code>${escapeHtml(x402PaidCallCommand)}</code></div>
1019
+ </section>
1020
+
500
1021
  <section class="card span-8">
501
1022
  <h2>Endpoints</h2>
502
1023
  <div class="endpoint-list">
503
1024
  <div class="endpoint-row"><span class="method">POST</span><code>${escapeHtml(info.endpoints.mcp)}</code></div>
1025
+ <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.docs)}</code></div>
504
1026
  <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.health)}</code></div>
505
1027
  <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.serverInfo)}</code></div>
1028
+ <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.openApi)}</code></div>
1029
+ <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.x402Discovery)}</code></div>
1030
+ <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.payShProvider)}</code></div>
506
1031
  <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.agentCard)}</code></div>
507
1032
  <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.wizardDescriptor)}</code></div>
508
1033
  </div>
@@ -522,6 +1047,115 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
522
1047
  </body>
523
1048
  </html>`;
524
1049
  }
1050
+ /**
1051
+ * @name buildDocsHtml
1052
+ * @description Builds the hosted SAP MCP documentation shell powered by Docsify.
1053
+ */
1054
+ export function buildDocsHtml(req, config) {
1055
+ const info = buildPublicServerInfo(req, config);
1056
+ const docsUrl = info.endpoints.docs;
1057
+ const title = 'SAP MCP Documentation | OOBE Protocol';
1058
+ const description = 'Install, configure, and operate SAP MCP with a focus on hosted remote access, local signer setup, x402/pay.sh payments, and MCP client configuration.';
1059
+ const docsifyConfig = JSON.stringify({
1060
+ name: 'SAP MCP',
1061
+ repo: 'https://github.com/OOBE-PROTOCOL/sap-mcp',
1062
+ homepage: 'README.md',
1063
+ loadSidebar: true,
1064
+ alias: {
1065
+ '/.*/_sidebar.md': '/docs/_sidebar.md',
1066
+ },
1067
+ subMaxLevel: 2,
1068
+ auto2top: true,
1069
+ themeColor: '#28d8e8',
1070
+ search: {
1071
+ paths: 'auto',
1072
+ placeholder: 'Search SAP MCP docs',
1073
+ noData: 'No results.',
1074
+ depth: 6,
1075
+ },
1076
+ }, null, 2).replace(/</g, '\\u003c');
1077
+ return `<!doctype html>
1078
+ <html lang="en">
1079
+ <head>
1080
+ <meta charset="utf-8">
1081
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1082
+ <title>${escapeHtml(title)}</title>
1083
+ <meta name="description" content="${escapeHtml(description)}">
1084
+ <meta name="robots" content="index,follow">
1085
+ <link rel="canonical" href="${escapeHtml(docsUrl)}">
1086
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
1087
+ <link rel="icon" type="image/png" href="/favicon.png">
1088
+ <link rel="shortcut icon" href="/favicon.ico">
1089
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
1090
+ <meta property="og:type" content="website">
1091
+ <meta property="og:title" content="${escapeHtml(title)}">
1092
+ <meta property="og:description" content="${escapeHtml(description)}">
1093
+ <meta property="og:url" content="${escapeHtml(docsUrl)}">
1094
+ <meta property="og:image" content="${escapeHtml(info.endpoints.favicon)}">
1095
+ <meta name="twitter:card" content="summary">
1096
+ <meta name="twitter:title" content="${escapeHtml(title)}">
1097
+ <meta name="twitter:description" content="${escapeHtml(description)}">
1098
+ <meta name="twitter:image" content="${escapeHtml(info.endpoints.favicon)}">
1099
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsify@4.13.1/lib/themes/vue.css">
1100
+ <style>
1101
+ :root {
1102
+ --theme-color: #28d8e8;
1103
+ --sidebar-width: 310px;
1104
+ }
1105
+ body {
1106
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1107
+ color: #17242b;
1108
+ }
1109
+ .app-name-link {
1110
+ color: #062730;
1111
+ font-weight: 800;
1112
+ }
1113
+ .sidebar {
1114
+ border-right: 1px solid rgba(6,39,48,.10);
1115
+ }
1116
+ .sidebar-nav strong,
1117
+ .markdown-section h1,
1118
+ .markdown-section h2,
1119
+ .markdown-section h3 {
1120
+ color: #062730;
1121
+ letter-spacing: 0;
1122
+ }
1123
+ .markdown-section a {
1124
+ color: #087f91;
1125
+ text-underline-offset: 3px;
1126
+ }
1127
+ .markdown-section code {
1128
+ color: #062730;
1129
+ border-radius: 6px;
1130
+ }
1131
+ .markdown-section pre {
1132
+ border-radius: 8px;
1133
+ border: 1px solid rgba(6,39,48,.10);
1134
+ }
1135
+ .markdown-section table {
1136
+ display: table;
1137
+ width: 100%;
1138
+ }
1139
+ .markdown-section blockquote {
1140
+ border-left-color: #28d8e8;
1141
+ color: #49656f;
1142
+ }
1143
+ .cover-main,
1144
+ section.cover {
1145
+ background: linear-gradient(115deg, rgba(40,216,232,.13), transparent 34%), #f8fdff;
1146
+ }
1147
+ </style>
1148
+ </head>
1149
+ <body>
1150
+ <div id="app">Loading SAP MCP documentation...</div>
1151
+ <script>
1152
+ window.$docsify = ${docsifyConfig};
1153
+ </script>
1154
+ <script src="https://cdn.jsdelivr.net/npm/docsify@4.13.1/lib/docsify.min.js" defer></script>
1155
+ <script src="https://cdn.jsdelivr.net/npm/docsify@4.13.1/lib/plugins/search.min.js" defer></script>
1156
+ </body>
1157
+ </html>`;
1158
+ }
525
1159
  /**
526
1160
  * @name acceptsHtmlPreview
527
1161
  * @description Returns true for browser-style GET previews without interfering with MCP SSE clients.
@@ -662,13 +1296,16 @@ export function buildWizardInstallDescriptor(req, config) {
662
1296
  ],
663
1297
  endpoints: {
664
1298
  descriptor: `${baseUrl}/.well-known/sap-mcp-wizard.json`,
1299
+ downloads: `${baseUrl}/wizard/downloads.json`,
665
1300
  installScript: `${baseUrl}/wizard/install.sh`,
666
1301
  mcp: `${baseUrl}/mcp`,
667
1302
  },
668
1303
  commands: {
669
- runWizard: 'npm exec --yes --package @oobe-protocol-labs/sap-mcp-server -- sap-mcp-config wizard',
1304
+ runWizard: WIZARD_NPM_COMMAND,
670
1305
  showConfig: 'npm exec --yes --package @oobe-protocol-labs/sap-mcp-server -- sap-mcp-config show',
671
1306
  showProfile: 'npm exec --yes --package @oobe-protocol-labs/sap-mcp-server -- sap-mcp-config profile-info',
1307
+ installX402PaidCallAddon: WIZARD_NPM_COMMAND,
1308
+ runX402PaidCall: X402_PAID_CALL_NPX_COMMAND,
672
1309
  },
673
1310
  security: {
674
1311
  keypairBytesExposed: false,
@@ -700,6 +1337,44 @@ export function buildWizardInstallScript(req, config) {
700
1337
  '',
701
1338
  ].join('\n');
702
1339
  }
1340
+ /**
1341
+ * @name extractToolNamesFromJsonRpc
1342
+ * @description Extracts tool names from a parsed JSON-RPC 2.0 request body for request logging.
1343
+ */
1344
+ function extractToolNamesFromJsonRpc(body) {
1345
+ if (!body || typeof body !== 'object')
1346
+ return undefined;
1347
+ const record = body;
1348
+ // tools/call request: { method: "tools/call", params: { name: "tool_name", ... } }
1349
+ if (record.method === 'tools/call' && record.params && typeof record.params === 'object') {
1350
+ const params = record.params;
1351
+ if (typeof params.name === 'string')
1352
+ return [params.name];
1353
+ }
1354
+ // Batch request: array of requests
1355
+ if (Array.isArray(body)) {
1356
+ const names = [];
1357
+ for (const item of body) {
1358
+ const extracted = extractToolNamesFromJsonRpc(item);
1359
+ if (extracted)
1360
+ names.push(...extracted);
1361
+ }
1362
+ return names.length > 0 ? names : undefined;
1363
+ }
1364
+ return undefined;
1365
+ }
1366
+ /**
1367
+ * @name extractJsonRpcMethod
1368
+ * @description Extracts the JSON-RPC method name from a parsed request body for request logging.
1369
+ */
1370
+ function extractJsonRpcMethod(body) {
1371
+ if (!body || typeof body !== 'object')
1372
+ return undefined;
1373
+ const record = body;
1374
+ if (typeof record.method === 'string')
1375
+ return record.method;
1376
+ return undefined;
1377
+ }
703
1378
  /**
704
1379
  * @name RemoteMCPServer
705
1380
  * @description Remote MCP server backed by the official Streamable HTTP transport.
@@ -709,7 +1384,7 @@ export class RemoteMCPServer {
709
1384
  appConfig;
710
1385
  authManager;
711
1386
  rateLimiter;
712
- transport;
1387
+ statefulSessions = new Map();
713
1388
  httpServer;
714
1389
  constructor(config) {
715
1390
  this.appConfig = loadConfig();
@@ -730,63 +1405,88 @@ export class RemoteMCPServer {
730
1405
  this.validateAuthConfig();
731
1406
  await this.rateLimiter.initialize();
732
1407
  const monetizationGate = await McpMonetizationGate.create(this.appConfig);
733
- // In stateful mode, create one long-lived transport and connect the server once.
734
- // In stateless mode, a fresh transport + server must be created per request —
735
- // the MCP SDK throws "Stateless transport cannot be reused across requests"
736
- // if a stateless transport handles more than one request.
737
- if (!this.config.stateless) {
738
- const server = await createSapMcpServer(this.appConfig);
739
- this.transport = new StreamableHTTPServerTransport({
740
- sessionIdGenerator: () => randomUUID(),
741
- });
742
- await server.connect(this.transport);
743
- }
744
1408
  this.httpServer = http.createServer(async (req, res) => {
745
1409
  if (this.applyCors(req, res)) {
746
1410
  return;
747
1411
  }
748
1412
  const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
749
- if (req.method === 'GET' && url.pathname === '/') {
750
- writeHtml(res, 200, buildLandingHtml(req, this.config));
1413
+ if (isPublicReadMethod(req.method) && url.pathname === '/') {
1414
+ writeHtml(res, 200, buildLandingHtml(req, this.config), isHeadMethod(req.method));
1415
+ return;
1416
+ }
1417
+ if (isPublicReadMethod(req.method) && ['/docs', '/docs/', '/docs/index.html'].includes(url.pathname)) {
1418
+ writeHtml(res, 200, buildDocsHtml(req, this.config), isHeadMethod(req.method));
751
1419
  return;
752
1420
  }
753
- if (req.method === 'GET' && url.pathname === '/server.json') {
1421
+ if (isPublicReadMethod(req.method) && url.pathname === '/server.json') {
754
1422
  writeJson(res, 200, buildPublicServerInfo(req, this.config), {
755
1423
  'Cache-Control': 'public, max-age=300',
756
- });
1424
+ }, isHeadMethod(req.method));
757
1425
  return;
758
1426
  }
759
- if (req.method === 'GET' && ['/favicon.png', '/favicon.ico', '/apple-touch-icon.png', '/og.png'].includes(url.pathname)) {
760
- writePngAsset(res);
1427
+ const publicLogoAsset = resolvePublicLogoAsset(req.method, url.pathname);
1428
+ if (publicLogoAsset) {
1429
+ writeLogoAsset(res, publicLogoAsset);
761
1430
  return;
762
1431
  }
763
- if (req.method === 'GET' && url.pathname === '/health') {
1432
+ const publicDocsMarkdown = resolvePublicDocsMarkdown(req.method, url.pathname);
1433
+ if (publicDocsMarkdown) {
1434
+ writeMarkdownAsset(res, publicDocsMarkdown);
1435
+ return;
1436
+ }
1437
+ if (isPublicReadMethod(req.method) && url.pathname === '/health') {
764
1438
  writeJson(res, 200, {
765
1439
  status: 'ok',
766
1440
  service: 'sap-mcp-remote',
767
1441
  transport: 'streamable-http',
768
1442
  timestamp: new Date().toISOString(),
769
- });
1443
+ }, {}, isHeadMethod(req.method));
770
1444
  return;
771
1445
  }
772
- if (req.method === 'GET' && ['/.well-known/agent-card.json', '/a2a/agent-card'].includes(url.pathname)) {
773
- writeJson(res, 200, buildA2AAgentCard(req, this.config));
1446
+ if (isPublicReadMethod(req.method) && ['/.well-known/agent-card.json', '/a2a/agent-card'].includes(url.pathname)) {
1447
+ writeJson(res, 200, buildA2AAgentCard(req, this.config), {}, isHeadMethod(req.method));
774
1448
  return;
775
1449
  }
776
- if (req.method === 'GET' && ['/.well-known/sap-mcp-wizard.json', '/wizard', '/wizard.json'].includes(url.pathname)) {
777
- writeJson(res, 200, buildWizardInstallDescriptor(req, this.config));
1450
+ if (isPublicReadMethod(req.method) && ['/.well-known/sap-mcp-wizard.json', '/wizard', '/wizard.json'].includes(url.pathname)) {
1451
+ writeJson(res, 200, buildWizardInstallDescriptor(req, this.config), {}, isHeadMethod(req.method));
778
1452
  return;
779
1453
  }
780
- if (req.method === 'GET' && url.pathname === '/wizard/install.sh') {
1454
+ if (isPublicReadMethod(req.method) && url.pathname === '/wizard/downloads.json') {
1455
+ const info = buildPublicServerInfo(req, this.config);
1456
+ writeJson(res, 200, info.downloads, {
1457
+ 'Cache-Control': 'public, max-age=300',
1458
+ }, isHeadMethod(req.method));
1459
+ return;
1460
+ }
1461
+ if (isPublicReadMethod(req.method) && url.pathname === '/openapi.json') {
1462
+ writeJson(res, 200, buildOpenApiSpec(req, this.config), {
1463
+ 'Cache-Control': 'public, max-age=300',
1464
+ }, isHeadMethod(req.method));
1465
+ return;
1466
+ }
1467
+ if (isPublicReadMethod(req.method) && url.pathname === '/pay/provider.yml') {
1468
+ const providerYaml = buildPublicPayShProviderYaml(req, this.config, this.appConfig);
1469
+ writeText(res, 200, providerYaml, 'application/yaml; charset=utf-8', {}, isHeadMethod(req.method));
1470
+ return;
1471
+ }
1472
+ if (isPublicReadMethod(req.method) && url.pathname === '/.well-known/x402') {
1473
+ writeJson(res, 200, buildX402DiscoveryDocument(req, this.config), {
1474
+ 'Cache-Control': 'public, max-age=300',
1475
+ }, isHeadMethod(req.method));
1476
+ return;
1477
+ }
1478
+ if (isPublicReadMethod(req.method) && url.pathname === '/wizard/install.sh') {
1479
+ const script = buildWizardInstallScript(req, this.config);
781
1480
  res.writeHead(200, {
782
1481
  'Content-Type': 'text/x-shellscript; charset=utf-8',
783
1482
  'Cache-Control': 'public, max-age=300',
1483
+ 'Content-Length': Buffer.byteLength(script),
784
1484
  });
785
- res.end(buildWizardInstallScript(req, this.config));
1485
+ res.end(isHeadMethod(req.method) ? undefined : script);
786
1486
  return;
787
1487
  }
788
- if (req.method === 'GET' && url.pathname === '/mcp' && acceptsHtmlPreview(req)) {
789
- writeHtml(res, 200, buildLandingHtml(req, this.config, 'mcp'));
1488
+ if (isPublicReadMethod(req.method) && url.pathname === '/mcp' && (isHeadMethod(req.method) || acceptsHtmlPreview(req))) {
1489
+ writeHtml(res, 200, buildLandingHtml(req, this.config, 'mcp'), isHeadMethod(req.method));
790
1490
  return;
791
1491
  }
792
1492
  if (url.pathname !== '/mcp') {
@@ -813,6 +1513,26 @@ export class RemoteMCPServer {
813
1513
  });
814
1514
  return;
815
1515
  }
1516
+ // ── Request logging ─────────────────────────────────────────────
1517
+ const reqStart = Date.now();
1518
+ const callerIp = getRateLimitKey(req);
1519
+ const originalEnd = res.end.bind(res);
1520
+ let loggedMethod;
1521
+ let loggedToolNames;
1522
+ res.end = ((...args) => {
1523
+ const latencyMs = Date.now() - reqStart;
1524
+ const statusCode = res.statusCode;
1525
+ logger.info('MCP request', {
1526
+ method: req.method,
1527
+ path: url.pathname,
1528
+ status: statusCode,
1529
+ latencyMs,
1530
+ callerIp,
1531
+ toolNames: loggedToolNames,
1532
+ rpcMethod: loggedMethod,
1533
+ });
1534
+ return originalEnd(...args);
1535
+ });
816
1536
  try {
817
1537
  if (this.config.stateless) {
818
1538
  // Stateless mode: create a fresh transport + server per request.
@@ -824,22 +1544,32 @@ export class RemoteMCPServer {
824
1544
  await perRequestServer.connect(perRequestTransport);
825
1545
  if (monetizationGate) {
826
1546
  await monetizationGate.handle(req, res, async (mcpReq, mcpRes, parsedBody) => {
1547
+ // Extract tool names from parsed JSON-RPC body for logging
1548
+ loggedToolNames = extractToolNamesFromJsonRpc(parsedBody);
1549
+ loggedMethod = extractJsonRpcMethod(parsedBody);
827
1550
  await perRequestTransport.handleRequest(mcpReq, mcpRes, parsedBody);
828
1551
  });
829
1552
  }
830
1553
  else {
1554
+ // Parse body for logging before passing to transport
1555
+ // The transport will re-read it from the stream
1556
+ loggedToolNames = undefined; // Will be logged without tool names in free mode
831
1557
  await perRequestTransport.handleRequest(req, res);
832
1558
  }
833
1559
  }
834
1560
  else {
835
- // Stateful mode: reuse the long-lived transport.
1561
+ // Stateful mode: route each request to the transport created by its initialize response.
836
1562
  if (monetizationGate) {
837
1563
  await monetizationGate.handle(req, res, async (mcpReq, mcpRes, parsedBody) => {
838
- await this.transport?.handleRequest(mcpReq, mcpRes, parsedBody);
1564
+ loggedToolNames = extractToolNamesFromJsonRpc(parsedBody);
1565
+ loggedMethod = extractJsonRpcMethod(parsedBody);
1566
+ await this.handleStatefulMcpRequest(mcpReq, mcpRes, parsedBody);
1567
+ }, {
1568
+ validatePaidRequest: context => this.validateStatefulPaidRequest(context.request, context.parsedBody),
839
1569
  });
840
1570
  }
841
1571
  else {
842
- await this.transport?.handleRequest(req, res);
1572
+ await this.handleStatefulMcpRequest(req, res);
843
1573
  }
844
1574
  }
845
1575
  }
@@ -878,7 +1608,9 @@ export class RemoteMCPServer {
878
1608
  * @description Gracefully closes the MCP transport and HTTP listener.
879
1609
  */
880
1610
  async stop() {
881
- await this.transport?.close();
1611
+ const sessions = Array.from(this.statefulSessions.values());
1612
+ this.statefulSessions.clear();
1613
+ await Promise.allSettled(sessions.map(session => session.transport.close()));
882
1614
  if (!this.httpServer) {
883
1615
  return;
884
1616
  }
@@ -906,6 +1638,110 @@ export class RemoteMCPServer {
906
1638
  throw new Error('Remote MCP JWT auth requires SAP_MCP_AUTH_SECRET');
907
1639
  }
908
1640
  }
1641
+ /**
1642
+ * @name handleStatefulMcpRequest
1643
+ * @description Routes a stateful MCP request to the session transport created by initialize.
1644
+ */
1645
+ async handleStatefulMcpRequest(req, res, parsedBody) {
1646
+ const transport = await this.resolveStatefulTransport(req, parsedBody);
1647
+ if (!transport) {
1648
+ const providedSessionId = readSingleHeader(req.headers['mcp-session-id']);
1649
+ writeMcpJsonRpcError(res, providedSessionId ? 404 : 400, providedSessionId ? -32011 : -32010, extractJsonRpcId(parsedBody), providedSessionId ? 'mcp_session_not_found' : 'mcp_session_required', {
1650
+ requiredHeader: 'mcp-session-id',
1651
+ ...(providedSessionId ? { providedSessionId } : {}),
1652
+ hint: providedSessionId
1653
+ ? 'The mcp-session-id must come from this server initialize response. Client-generated UUIDs are not valid MCP sessions.'
1654
+ : 'Call initialize first and reuse the returned mcp-session-id header. Do not generate a client-side UUID.',
1655
+ });
1656
+ return;
1657
+ }
1658
+ await transport.handleRequest(req, res, parsedBody);
1659
+ }
1660
+ /**
1661
+ * @name validateStatefulPaidRequest
1662
+ * @description Fails paid tool calls before x402 verification when the MCP session is missing or unknown.
1663
+ */
1664
+ validateStatefulPaidRequest(req, parsedBody) {
1665
+ if (isInitializeJsonRpcRequest(parsedBody)) {
1666
+ return undefined;
1667
+ }
1668
+ const sessionId = readSingleHeader(req.headers['mcp-session-id']);
1669
+ if (!sessionId) {
1670
+ return {
1671
+ code: -32010,
1672
+ message: 'mcp_session_required',
1673
+ data: {
1674
+ requiredHeader: 'mcp-session-id',
1675
+ hint: 'Initialize the MCP Streamable HTTP session first, then reuse the returned mcp-session-id for the unpaid payment challenge and the paid retry.',
1676
+ },
1677
+ };
1678
+ }
1679
+ if (!this.statefulSessions.has(sessionId)) {
1680
+ return {
1681
+ code: -32011,
1682
+ message: 'mcp_session_not_found',
1683
+ data: {
1684
+ requiredHeader: 'mcp-session-id',
1685
+ providedSessionId: sessionId,
1686
+ hint: 'The mcp-session-id must come from this server initialize response. Client-generated UUIDs are not valid MCP sessions.',
1687
+ },
1688
+ };
1689
+ }
1690
+ return undefined;
1691
+ }
1692
+ /**
1693
+ * @name resolveStatefulTransport
1694
+ * @description Returns the transport for an existing session or creates one for an initialize request.
1695
+ */
1696
+ async resolveStatefulTransport(req, parsedBody) {
1697
+ const sessionId = readSingleHeader(req.headers['mcp-session-id']);
1698
+ if (sessionId) {
1699
+ const session = this.statefulSessions.get(sessionId);
1700
+ if (!session) {
1701
+ return undefined;
1702
+ }
1703
+ session.lastSeenAt = Date.now();
1704
+ return session.transport;
1705
+ }
1706
+ if (req.method === 'POST' && (parsedBody === undefined || isInitializeJsonRpcRequest(parsedBody))) {
1707
+ return await this.createStatefulSessionTransport();
1708
+ }
1709
+ return undefined;
1710
+ }
1711
+ /**
1712
+ * @name createStatefulSessionTransport
1713
+ * @description Creates one MCP server and Streamable HTTP transport for a single initialized session.
1714
+ */
1715
+ async createStatefulSessionTransport() {
1716
+ const server = await createSapMcpServer(this.appConfig);
1717
+ const transport = new StreamableHTTPServerTransport({
1718
+ sessionIdGenerator: () => randomUUID(),
1719
+ onsessioninitialized: sessionId => {
1720
+ this.statefulSessions.set(sessionId, {
1721
+ server,
1722
+ transport,
1723
+ createdAt: Date.now(),
1724
+ lastSeenAt: Date.now(),
1725
+ });
1726
+ logger.info('Stateful MCP session initialized', {
1727
+ sessionId,
1728
+ activeSessions: this.statefulSessions.size,
1729
+ });
1730
+ },
1731
+ });
1732
+ transport.onclose = () => {
1733
+ const sessionId = transport.sessionId;
1734
+ if (sessionId) {
1735
+ this.statefulSessions.delete(sessionId);
1736
+ logger.info('Stateful MCP session closed', {
1737
+ sessionId,
1738
+ activeSessions: this.statefulSessions.size,
1739
+ });
1740
+ }
1741
+ };
1742
+ await server.connect(transport);
1743
+ return transport;
1744
+ }
909
1745
  /**
910
1746
  * @name applyCors
911
1747
  * @description Applies configured CORS headers and handles preflight requests.