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

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 +222 -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 +930 -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,23 @@ 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';
33
+ const MCP_REGISTRY_AUTH_PATH = '/.well-known/mcp-registry-auth';
23
34
  let logoAssetCache;
24
35
  let paymentStatsCache;
25
36
  /**
@@ -53,8 +64,13 @@ export function defaultRemoteConfig(appConfig) {
53
64
  port,
54
65
  host,
55
66
  corsOrigins: appConfig.httpCorsOrigins,
56
- stateless: parseRemoteBoolean(process.env.SAP_MCP_HTTP_STATELESS, appConfig.mode === 'hosted-api'),
67
+ stateless: parseRemoteBoolean(process.env.SAP_MCP_HTTP_STATELESS, false),
57
68
  rateLimit: buildRemoteRateLimitConfigFromEnv(appConfig.rateLimitPerMinute),
69
+ paymentDiscovery: buildPublicPaymentDiscovery(appConfig),
70
+ paymentPriceRange: {
71
+ minUsd: appConfig.monetization.prices.minUsd,
72
+ maxUsd: appConfig.monetization.prices.maxUsd,
73
+ },
58
74
  };
59
75
  if (authType === 'none') {
60
76
  return {
@@ -86,6 +102,56 @@ export function defaultRemoteConfig(appConfig) {
86
102
  },
87
103
  };
88
104
  }
105
+ /**
106
+ * @name buildPublicPaymentDiscovery
107
+ * @description Builds public x402 seller/facilitator coordinates without exposing facilitator URL or auth tokens.
108
+ */
109
+ function buildPublicPaymentDiscovery(appConfig) {
110
+ if (!appConfig.monetization.enabled || !appConfig.monetization.payTo) {
111
+ return undefined;
112
+ }
113
+ const network = resolvePaymentNetwork(appConfig);
114
+ const facilitatorPublicKey = (process.env.SAP_MCP_X402_FACILITATOR_PUBLIC_KEY
115
+ ?? process.env.SAP_MCP_X402_FACILITATOR_FEE_PAYER
116
+ ?? '').trim();
117
+ return {
118
+ provider: appConfig.monetization.provider,
119
+ network,
120
+ asset: resolvePublicPaymentAsset(network),
121
+ assetSymbol: 'USDC',
122
+ payTo: appConfig.monetization.payTo,
123
+ seller: {
124
+ name: 'OOBE Protocol',
125
+ payTo: appConfig.monetization.payTo,
126
+ },
127
+ ...(facilitatorPublicKey
128
+ ? {
129
+ facilitator: {
130
+ role: 'x402-svm-fee-payer',
131
+ publicKey: facilitatorPublicKey,
132
+ },
133
+ }
134
+ : {}),
135
+ headers: {
136
+ paymentRequired: 'PAYMENT-REQUIRED',
137
+ paymentSignature: 'PAYMENT-SIGNATURE',
138
+ paymentResponse: 'PAYMENT-RESPONSE',
139
+ },
140
+ };
141
+ }
142
+ /**
143
+ * @name resolvePublicPaymentAsset
144
+ * @description Resolves the public USDC mint advertised in x402 discovery metadata.
145
+ */
146
+ function resolvePublicPaymentAsset(network) {
147
+ if (network === SOLANA_DEVNET_CAIP2) {
148
+ return USDC_DEVNET_MINT;
149
+ }
150
+ return USDC_MAINNET_MINT;
151
+ }
152
+ function formatOpenApiUsd(value) {
153
+ return value.toFixed(6).replace(/0+$/, '').replace(/\.$/, '');
154
+ }
89
155
  function parseRemoteBoolean(raw, fallback) {
90
156
  if (raw === undefined) {
91
157
  return fallback;
@@ -96,38 +162,248 @@ function parseRemoteBoolean(raw, fallback) {
96
162
  * @name writeJson
97
163
  * @description Writes a JSON response with optional additional headers.
98
164
  */
99
- function writeJson(res, status, body, headers = {}) {
165
+ function writeJson(res, status, body, headers = {}, omitBody = false) {
166
+ const serialized = JSON.stringify(body);
100
167
  res.writeHead(status, {
101
168
  'Content-Type': 'application/json',
169
+ 'Content-Length': Buffer.byteLength(serialized),
102
170
  ...headers,
103
171
  });
104
- res.end(JSON.stringify(body));
172
+ res.end(omitBody ? undefined : serialized);
173
+ }
174
+ /**
175
+ * @name writeText
176
+ * @description Writes a cacheable text response with an explicit content type.
177
+ */
178
+ function writeText(res, status, body, contentType, headers = {}, omitBody = false) {
179
+ res.writeHead(status, {
180
+ 'Content-Type': contentType,
181
+ 'Cache-Control': 'public, max-age=300',
182
+ 'Content-Length': Buffer.byteLength(body),
183
+ 'X-Content-Type-Options': 'nosniff',
184
+ ...headers,
185
+ });
186
+ res.end(omitBody ? undefined : body);
187
+ }
188
+ /**
189
+ * @name writeMcpJsonRpcError
190
+ * @description Writes an MCP-compatible JSON-RPC error response with HTTP status semantics.
191
+ */
192
+ function writeMcpJsonRpcError(res, status, code, id, message, data) {
193
+ writeJson(res, status, {
194
+ jsonrpc: '2.0',
195
+ id,
196
+ error: {
197
+ code,
198
+ message,
199
+ ...(data === undefined ? {} : { data }),
200
+ },
201
+ });
202
+ }
203
+ /**
204
+ * @name readSingleHeader
205
+ * @description Reads one normalized HTTP header value without accepting ambiguous arrays.
206
+ */
207
+ function readSingleHeader(value) {
208
+ if (Array.isArray(value)) {
209
+ return value[0]?.trim() || undefined;
210
+ }
211
+ return value?.trim() || undefined;
212
+ }
213
+ /**
214
+ * @name extractJsonRpcId
215
+ * @description Extracts a JSON-RPC id for error correlation.
216
+ */
217
+ function extractJsonRpcId(body) {
218
+ if (Array.isArray(body) && body.length > 0) {
219
+ return extractJsonRpcId(body[0]);
220
+ }
221
+ if (!body || typeof body !== 'object') {
222
+ return null;
223
+ }
224
+ const record = body;
225
+ return typeof record.id === 'string' || typeof record.id === 'number' ? record.id : null;
226
+ }
227
+ /**
228
+ * @name isInitializeJsonRpcRequest
229
+ * @description Detects MCP initialize requests that are allowed to create a new stateful session.
230
+ */
231
+ function isInitializeJsonRpcRequest(body) {
232
+ if (Array.isArray(body)) {
233
+ return body.some(isInitializeJsonRpcRequest);
234
+ }
235
+ if (!body || typeof body !== 'object') {
236
+ return false;
237
+ }
238
+ return body.method === 'initialize';
105
239
  }
106
240
  /**
107
241
  * @name writeHtml
108
242
  * @description Writes a cacheable HTML response for public endpoint previews and social sharing.
109
243
  */
110
- function writeHtml(res, status, html) {
244
+ function writeHtml(res, status, html, omitBody = false) {
111
245
  res.writeHead(status, {
112
246
  'Content-Type': 'text/html; charset=utf-8',
113
247
  'Cache-Control': 'public, max-age=300',
248
+ 'Content-Length': Buffer.byteLength(html),
249
+ 'X-Content-Type-Options': 'nosniff',
250
+ });
251
+ res.end(omitBody ? undefined : html);
252
+ }
253
+ /**
254
+ * @name isPublicReadMethod
255
+ * @description Returns true for cacheable public metadata reads, including crawler-friendly HEAD checks.
256
+ */
257
+ function isPublicReadMethod(method) {
258
+ return method === 'GET' || method === 'HEAD';
259
+ }
260
+ /**
261
+ * @name isHeadMethod
262
+ * @description Returns true when the request must emit headers without a response body.
263
+ */
264
+ function isHeadMethod(method) {
265
+ return method === 'HEAD';
266
+ }
267
+ /**
268
+ * @name resolveMcpRegistryAuthRecord
269
+ * @description Resolves the MCP Registry HTTP ownership proof from a public env value or a private file path.
270
+ */
271
+ function resolveMcpRegistryAuthRecord() {
272
+ const inlineRecord = process.env.SAP_MCP_REGISTRY_AUTH_RECORD?.trim();
273
+ if (inlineRecord) {
274
+ return inlineRecord;
275
+ }
276
+ const recordPath = process.env.SAP_MCP_REGISTRY_AUTH_FILE?.trim();
277
+ if (!recordPath) {
278
+ return undefined;
279
+ }
280
+ try {
281
+ if (!existsSync(recordPath)) {
282
+ return undefined;
283
+ }
284
+ const fileRecord = readFileSync(recordPath, 'utf8').trim();
285
+ return fileRecord || undefined;
286
+ }
287
+ catch (error) {
288
+ logger.warn('Failed to read MCP Registry auth record', {
289
+ path: recordPath,
290
+ error: error instanceof Error ? error.message : String(error),
291
+ });
292
+ return undefined;
293
+ }
294
+ }
295
+ /**
296
+ * @name writeMcpRegistryAuthRecord
297
+ * @description Writes the MCP Registry HTTP ownership proof without exposing private signing material.
298
+ */
299
+ function writeMcpRegistryAuthRecord(res, omitBody) {
300
+ const record = resolveMcpRegistryAuthRecord();
301
+ if (!record) {
302
+ writeText(res, 404, 'MCP Registry auth record is not configured.\n', 'text/plain; charset=utf-8', { 'Cache-Control': 'no-store' }, omitBody);
303
+ return;
304
+ }
305
+ writeText(res, 200, `${record}\n`, 'text/plain; charset=utf-8', { 'Cache-Control': 'public, max-age=60' }, omitBody);
306
+ }
307
+ /**
308
+ * @name writeMarkdownAsset
309
+ * @description Writes a resolved public markdown documentation asset.
310
+ */
311
+ function writeMarkdownAsset(res, asset) {
312
+ res.writeHead(200, {
313
+ 'Content-Type': asset.contentType,
314
+ 'Cache-Control': 'public, max-age=120',
315
+ 'Content-Length': asset.contentLength,
114
316
  'X-Content-Type-Options': 'nosniff',
115
317
  });
116
- res.end(html);
318
+ res.end(asset.body);
319
+ }
320
+ /**
321
+ * @name resolvePublicDocsMarkdown
322
+ * @description Resolves safe hosted documentation markdown requests under /docs and /docs/user.
323
+ */
324
+ export function resolvePublicDocsMarkdown(method, pathname) {
325
+ if (method !== 'GET' && method !== 'HEAD') {
326
+ return undefined;
327
+ }
328
+ if (!pathname.startsWith('/docs/') || !pathname.endsWith('.md')) {
329
+ return undefined;
330
+ }
331
+ let decodedPath;
332
+ try {
333
+ decodedPath = decodeURIComponent(pathname.slice('/docs/'.length));
334
+ }
335
+ catch {
336
+ return undefined;
337
+ }
338
+ const normalizedPath = posix.normalize(decodedPath.replace(/\\/g, '/')).replace(/^[/\\]+/, '');
339
+ if (isUnsafeDocsPath(normalizedPath)) {
340
+ return undefined;
341
+ }
342
+ const root = normalizedPath.startsWith('user/')
343
+ ? USER_DOCS_ROOT_PATH
344
+ : DOCS_ROOT_PATH;
345
+ const relativePath = normalizedPath.startsWith('user/')
346
+ ? normalizedPath.slice('user/'.length)
347
+ : normalizedPath;
348
+ if (isUnsafeDocsPath(relativePath)) {
349
+ return undefined;
350
+ }
351
+ const absolutePath = join(root, relativePath);
352
+ if (!existsSync(absolutePath)) {
353
+ return undefined;
354
+ }
355
+ const markdown = readFileSync(absolutePath, 'utf-8');
356
+ return {
357
+ contentType: 'text/markdown; charset=utf-8',
358
+ contentLength: Buffer.byteLength(markdown),
359
+ body: method === 'HEAD' ? undefined : markdown,
360
+ };
361
+ }
362
+ function isUnsafeDocsPath(pathname) {
363
+ return pathname === ''
364
+ || pathname.includes('\0')
365
+ || pathname === '..'
366
+ || pathname.startsWith('../')
367
+ || pathname.includes('/../');
117
368
  }
118
369
  /**
119
- * @name writePngAsset
120
- * @description Writes the bundled SAP logo as a public icon/social image asset.
370
+ * @name resolvePublicLogoAsset
371
+ * @description Resolves public icon/social image routes, including HEAD-safe favicon responses for crawlers.
121
372
  */
122
- function writePngAsset(res) {
373
+ export function resolvePublicLogoAsset(method, pathname) {
374
+ if (method !== 'GET' && method !== 'HEAD') {
375
+ return undefined;
376
+ }
123
377
  const image = readLogoAsset();
378
+ const body = method === 'HEAD' ? undefined : image;
379
+ if (['/favicon.png', '/apple-touch-icon.png', '/og.png'].includes(pathname)) {
380
+ return {
381
+ contentType: 'image/png',
382
+ contentLength: image.byteLength,
383
+ body,
384
+ };
385
+ }
386
+ if (pathname === '/favicon.ico') {
387
+ return {
388
+ contentType: 'image/x-icon',
389
+ contentLength: image.byteLength,
390
+ body,
391
+ };
392
+ }
393
+ return undefined;
394
+ }
395
+ /**
396
+ * @name writeLogoAsset
397
+ * @description Writes a resolved public logo asset response.
398
+ */
399
+ function writeLogoAsset(res, asset) {
124
400
  res.writeHead(200, {
125
- 'Content-Type': 'image/png',
401
+ 'Content-Type': asset.contentType,
126
402
  'Cache-Control': 'public, max-age=86400',
127
- 'Content-Length': image.byteLength,
403
+ 'Content-Length': asset.contentLength,
128
404
  'X-Content-Type-Options': 'nosniff',
129
405
  });
130
- res.end(image);
406
+ res.end(asset.body);
131
407
  }
132
408
  /**
133
409
  * @name readLogoAsset
@@ -265,13 +541,27 @@ export function buildPublicServerInfo(req, config) {
265
541
  },
266
542
  endpoints: {
267
543
  landing: `${baseUrl}/`,
544
+ docs: `${baseUrl}/docs`,
268
545
  mcp: `${baseUrl}/mcp`,
269
546
  health: `${baseUrl}/health`,
270
547
  serverInfo: `${baseUrl}/server.json`,
548
+ openApi: `${baseUrl}/openapi.json`,
549
+ x402Discovery: `${baseUrl}/.well-known/x402`,
550
+ payShProvider: `${baseUrl}/pay/provider.yml`,
271
551
  agentCard: `${baseUrl}/.well-known/agent-card.json`,
272
552
  wizardDescriptor: `${baseUrl}/.well-known/sap-mcp-wizard.json`,
553
+ wizardDownloads: `${baseUrl}/wizard/downloads.json`,
273
554
  wizardInstallScript: `${baseUrl}/wizard/install.sh`,
274
555
  favicon: `${baseUrl}/favicon.png`,
556
+ faviconIco: `${baseUrl}/favicon.ico`,
557
+ },
558
+ downloads: {
559
+ release: `https://github.com/OOBE-PROTOCOL/sap-mcp/releases/tag/${MCP_SERVER_VERSION}`,
560
+ desktopWizard: {
561
+ macosArm64Dmg: `${RELEASE_DOWNLOAD_BASE_URL}/SAP-MCP-Wizard-${MCP_SERVER_VERSION}-arm64.dmg`,
562
+ windowsX64Setup: `${RELEASE_DOWNLOAD_BASE_URL}/SAP-MCP-Wizard-Setup-${MCP_SERVER_VERSION}-x64.exe`,
563
+ linuxX64TarGz: `${RELEASE_DOWNLOAD_BASE_URL}/sap-mcp-wizard-${MCP_SERVER_VERSION}-x64.tar.gz`,
564
+ },
275
565
  },
276
566
  capabilities: {
277
567
  tools: true,
@@ -285,6 +575,7 @@ export function buildPublicServerInfo(req, config) {
285
575
  schemes: authRequired ? ['Bearer'] : ['none', 'x402'],
286
576
  bearerRequired: authRequired,
287
577
  },
578
+ ...(config.paymentDiscovery ? { payments: config.paymentDiscovery } : {}),
288
579
  security: {
289
580
  keypairBytesExposed: false,
290
581
  storesUserKeypairs: false,
@@ -298,6 +589,237 @@ export function buildPublicServerInfo(req, config) {
298
589
  },
299
590
  };
300
591
  }
592
+ /**
593
+ * @name buildPublicPayShProviderYaml
594
+ * @description Builds a public pay.sh provider catalog for the hosted SAP MCP endpoint without RPC secrets or signer paths.
595
+ */
596
+ export function buildPublicPayShProviderYaml(req, config, appConfig) {
597
+ const baseUrl = buildPublicBaseUrl(req, config);
598
+ return generatePayShProviderYaml(appConfig, {
599
+ name: 'oobe-sap-mcp',
600
+ subdomain: 'oobe-sap-mcp',
601
+ title: 'OOBE SAP MCP Server',
602
+ description: PUBLIC_SERVER_DESCRIPTION,
603
+ version: MCP_SERVER_VERSION,
604
+ upstreamUrl: baseUrl,
605
+ recipient: config.paymentDiscovery?.payTo ?? appConfig.monetization.payTo,
606
+ rpcUrl: undefined,
607
+ signerPath: undefined,
608
+ });
609
+ }
610
+ /**
611
+ * @name buildOpenApiSpec
612
+ * @description OpenAPI 3.1 spec for x402scan discovery. Describes the /mcp endpoint
613
+ * with x-payment-info so x402scan can discover and register the API.
614
+ */
615
+ export function buildOpenApiSpec(req, config) {
616
+ const baseUrl = buildPublicBaseUrl(req, config);
617
+ const priceRange = config.paymentPriceRange ?? { minUsd: 0.001, maxUsd: 100 };
618
+ return {
619
+ openapi: '3.1.0',
620
+ info: {
621
+ title: 'SAP MCP Server',
622
+ version: MCP_SERVER_VERSION,
623
+ description: 'MCP gateway for Solana DeFi, SAP agent registry, and x402 micropayments. 248 tools across Jupiter, Drift, Metaplex, Pump.fun, Raydium, Orca, and more.',
624
+ '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.',
625
+ contact: {
626
+ name: 'OOBE Protocol Labs',
627
+ url: 'https://github.com/OOBE-PROTOCOL/sap-mcp',
628
+ email: 'oobe@oobeprotocol.ai',
629
+ },
630
+ },
631
+ 'x-discovery': {
632
+ resources: [`${baseUrl}/mcp`],
633
+ openApi: `${baseUrl}/openapi.json`,
634
+ x402Discovery: `${baseUrl}/.well-known/x402`,
635
+ payShProvider: `${baseUrl}/pay/provider.yml`,
636
+ ...(config.paymentDiscovery ? { payments: config.paymentDiscovery } : {}),
637
+ },
638
+ 'x-pay-sh': {
639
+ providerYaml: `${baseUrl}/pay/provider.yml`,
640
+ routing: {
641
+ type: 'proxy',
642
+ upstream: `${baseUrl}/`,
643
+ },
644
+ monetization: {
645
+ mode: 'metered',
646
+ unit: 'requests',
647
+ sourceOfTruth: 'SAP MCP per-tool x402 pricing remains authoritative after proxy forwarding.',
648
+ },
649
+ },
650
+ servers: [
651
+ { url: baseUrl, description: 'SAP MCP Hosted Server' },
652
+ ],
653
+ paths: {
654
+ '/mcp': {
655
+ post: {
656
+ operationId: 'mcpCall',
657
+ summary: 'Fetch SAP MCP JSON-RPC response',
658
+ tags: ['MCP'],
659
+ 'x-payment-info': {
660
+ price: {
661
+ mode: 'dynamic',
662
+ currency: 'USD',
663
+ min: formatOpenApiUsd(priceRange.minUsd),
664
+ max: formatOpenApiUsd(priceRange.maxUsd),
665
+ },
666
+ protocols: ['x402'],
667
+ ...(config.paymentDiscovery
668
+ ? {
669
+ network: config.paymentDiscovery.network,
670
+ asset: config.paymentDiscovery.asset,
671
+ assetSymbol: config.paymentDiscovery.assetSymbol,
672
+ payTo: config.paymentDiscovery.payTo,
673
+ seller: config.paymentDiscovery.seller,
674
+ facilitator: config.paymentDiscovery.facilitator,
675
+ }
676
+ : {}),
677
+ },
678
+ requestBody: {
679
+ required: true,
680
+ content: {
681
+ 'application/json': {
682
+ schema: {
683
+ oneOf: [
684
+ {
685
+ type: 'object',
686
+ description: 'General MCP JSON-RPC request such as initialize, tools/list, prompts/list, resources/list, or ping.',
687
+ properties: {
688
+ jsonrpc: { type: 'string', const: '2.0' },
689
+ id: { oneOf: [{ type: 'integer' }, { type: 'string' }], description: 'JSON-RPC request ID' },
690
+ method: {
691
+ type: 'string',
692
+ description: 'MCP method.',
693
+ examples: ['initialize', 'tools/list', 'prompts/list', 'resources/list', 'ping'],
694
+ },
695
+ params: { type: 'object', additionalProperties: true },
696
+ },
697
+ required: ['jsonrpc', 'id', 'method'],
698
+ },
699
+ {
700
+ type: 'object',
701
+ description: 'MCP tools/call request. Paid tool calls may require x402 payment before execution.',
702
+ properties: {
703
+ jsonrpc: { type: 'string', const: '2.0' },
704
+ id: { oneOf: [{ type: 'integer' }, { type: 'string' }], description: 'JSON-RPC request ID' },
705
+ method: { type: 'string', const: 'tools/call' },
706
+ params: {
707
+ type: 'object',
708
+ description: 'MCP tools/call parameters containing the target tool name and optional tool arguments.',
709
+ properties: {
710
+ name: { type: 'string', description: 'Tool name (e.g. jupiter_getPrice)' },
711
+ arguments: { type: 'object', description: 'Tool arguments', additionalProperties: true },
712
+ },
713
+ required: ['name'],
714
+ additionalProperties: true,
715
+ },
716
+ },
717
+ required: ['jsonrpc', 'id', 'method', 'params'],
718
+ },
719
+ ],
720
+ },
721
+ },
722
+ },
723
+ },
724
+ responses: {
725
+ '200': {
726
+ description: 'MCP tool result or payment_required JSON-RPC error (for MCP SDK clients)',
727
+ content: {
728
+ 'application/json': {
729
+ schema: {
730
+ type: 'object',
731
+ properties: {
732
+ jsonrpc: { type: 'string' },
733
+ id: { type: 'integer' },
734
+ result: { type: 'object', description: 'Tool result (if free or paid)' },
735
+ error: {
736
+ type: 'object',
737
+ properties: {
738
+ code: { type: 'integer' },
739
+ message: { type: 'string' },
740
+ data: {
741
+ type: 'object',
742
+ properties: {
743
+ protocol: { type: 'string' },
744
+ paymentRequirements: { type: 'array' },
745
+ },
746
+ },
747
+ },
748
+ },
749
+ },
750
+ },
751
+ },
752
+ 'text/event-stream': {
753
+ schema: {
754
+ type: 'string',
755
+ description: 'Streamable HTTP MCP event stream containing JSON-RPC response events.',
756
+ },
757
+ },
758
+ },
759
+ },
760
+ '402': {
761
+ description: 'Payment Required (x402 challenge for non-MCP clients)',
762
+ content: {
763
+ 'application/json': {
764
+ schema: {
765
+ type: 'object',
766
+ properties: {
767
+ x402Version: { type: 'integer' },
768
+ error: { type: 'string' },
769
+ accepts: {
770
+ type: 'array',
771
+ items: {
772
+ type: 'object',
773
+ properties: {
774
+ scheme: { type: 'string' },
775
+ network: { type: 'string' },
776
+ amount: { type: 'string', description: 'USDC atomic units (6 decimals)' },
777
+ asset: { type: 'string' },
778
+ payTo: { type: 'string' },
779
+ maxTimeoutSeconds: { type: 'integer' },
780
+ extra: {
781
+ type: 'object',
782
+ properties: {
783
+ feePayer: { type: 'string' },
784
+ },
785
+ },
786
+ },
787
+ },
788
+ },
789
+ resource: {
790
+ type: 'object',
791
+ properties: {
792
+ url: { type: 'string' },
793
+ description: { type: 'string' },
794
+ mimeType: { type: 'string' },
795
+ serviceName: { type: 'string' },
796
+ tags: { type: 'array', items: { type: 'string' } },
797
+ },
798
+ },
799
+ },
800
+ },
801
+ },
802
+ },
803
+ },
804
+ },
805
+ },
806
+ },
807
+ },
808
+ };
809
+ }
810
+ /**
811
+ * @name buildX402DiscoveryDocument
812
+ * @description Builds the compatibility discovery document consumed by x402scan.
813
+ */
814
+ export function buildX402DiscoveryDocument(req, config) {
815
+ const baseUrl = buildPublicBaseUrl(req, config);
816
+ return {
817
+ version: 1,
818
+ resources: [`${baseUrl}/mcp`],
819
+ ...(config.paymentDiscovery ? { payments: config.paymentDiscovery } : {}),
820
+ 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.',
821
+ };
822
+ }
301
823
  /**
302
824
  * @name buildLandingHtml
303
825
  * @description Builds professional public HTML metadata for root and `/mcp` URL previews.
@@ -310,8 +832,9 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
310
832
  ? 'SAP MCP Streamable HTTP Endpoint | OOBE Protocol'
311
833
  : info.title;
312
834
  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';
835
+ const wizardCommand = WIZARD_NPM_COMMAND;
314
836
  const installScriptCommand = 'curl -fsSL https://mcp.sap.oobeprotocol.ai/wizard/install.sh | sh';
837
+ const x402PaidCallCommand = X402_PAID_CALL_NPX_COMMAND;
315
838
  const publicInfo = JSON.stringify(info, null, 2).replace(/</g, '\\u003c');
316
839
  return `<!doctype html>
317
840
  <html lang="en">
@@ -322,8 +845,9 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
322
845
  <meta name="description" content="${escapeHtml(info.description)}">
323
846
  <meta name="robots" content="index,follow">
324
847
  <link rel="canonical" href="${escapeHtml(pageUrl)}">
848
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
325
849
  <link rel="icon" type="image/png" href="/favicon.png">
326
- <link rel="shortcut icon" type="image/png" href="/favicon.png">
850
+ <link rel="shortcut icon" href="/favicon.ico">
327
851
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
328
852
  <meta property="og:type" content="website">
329
853
  <meta property="og:title" content="${escapeHtml(title)}">
@@ -428,6 +952,21 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
428
952
  .list { padding-left: 18px; margin: 0; color: var(--muted); }
429
953
  .list li + li { margin-top: 8px; }
430
954
  .command { margin-top: 12px; border: 1px solid rgba(255,255,255,.10); border-radius: 8px; background: rgba(0,0,0,.28); padding: 13px; }
955
+ .download-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 14px; }
956
+ .download-link {
957
+ display: grid;
958
+ min-height: 74px;
959
+ align-content: center;
960
+ gap: 4px;
961
+ border: 1px solid var(--line);
962
+ border-radius: 8px;
963
+ background: rgba(0,0,0,.18);
964
+ padding: 12px;
965
+ color: var(--ink);
966
+ text-decoration: none;
967
+ }
968
+ .download-link strong { color: var(--aqua); }
969
+ .download-link span { color: var(--muted); font-size: .88rem; }
431
970
  .endpoint-list { display: grid; gap: 10px; margin-top: 10px; }
432
971
  .endpoint-row { display: grid; grid-template-columns: 72px 1fr; gap: 12px; align-items: baseline; }
433
972
  .method { color: var(--success); font: 700 .78rem/1 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
@@ -436,6 +975,7 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
436
975
  header { grid-template-columns: 1fr; }
437
976
  img.logo { width: 76px; height: 76px; border-radius: 20px; }
438
977
  .span-3, .span-4, .span-5, .span-6, .span-7, .span-8 { grid-column: span 12; }
978
+ .download-grid { grid-template-columns: 1fr; }
439
979
  }
440
980
  @media (min-width: 861px) and (max-width: 1120px) {
441
981
  .span-3, .span-4 { grid-column: span 6; }
@@ -456,7 +996,11 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
456
996
  <p>${escapeHtml(info.description)}</p>
457
997
  <div class="hero-actions">
458
998
  <a class="button" href="/wizard/install.sh">Install wizard</a>
999
+ <a class="button secondary" href="/wizard/downloads.json">Downloads</a>
1000
+ <a class="button secondary" href="/docs">Docs</a>
459
1001
  <a class="button secondary" href="/server.json">Server JSON</a>
1002
+ <a class="button secondary" href="/openapi.json">OpenAPI</a>
1003
+ <a class="button secondary" href="/pay/provider.yml">pay.sh YAML</a>
460
1004
  <a class="button secondary" href="/.well-known/agent-card.json">Agent card</a>
461
1005
  </div>
462
1006
  </div>
@@ -488,21 +1032,43 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
488
1032
  <div class="command"><code>${escapeHtml(wizardCommand)}</code></div>
489
1033
  </section>
490
1034
 
1035
+ <section class="card span-5">
1036
+ <h2>Native Downloads</h2>
1037
+ <p>Use the desktop wizard when you want a guided setup without touching config files by hand.</p>
1038
+ <div class="download-grid">
1039
+ <a class="download-link" href="${escapeHtml(info.downloads.desktopWizard.windowsX64Setup)}"><strong>Windows</strong><span>x64 setup .exe</span></a>
1040
+ <a class="download-link" href="${escapeHtml(info.downloads.desktopWizard.macosArm64Dmg)}"><strong>macOS</strong><span>Apple Silicon .dmg</span></a>
1041
+ <a class="download-link" href="${escapeHtml(info.downloads.desktopWizard.linuxX64TarGz)}"><strong>Linux</strong><span>x64 tar.gz</span></a>
1042
+ </div>
1043
+ <p class="caption">Machine-readable links: <a href="${escapeHtml(info.endpoints.wizardDownloads)}">/wizard/downloads.json</a>.</p>
1044
+ </section>
1045
+
491
1046
  <section class="card span-5">
492
1047
  <h2>Payments</h2>
493
1048
  <ul class="list">
494
1049
  <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>
1050
+ <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
1051
  <li><strong>Local stdio:</strong> free by default; monetization applies to hosted remote usage.</li>
497
1052
  </ul>
498
1053
  </section>
499
1054
 
1055
+ <section class="card strong span-12">
1056
+ <h2>x402 Paid-Call Plugin For Agents</h2>
1057
+ <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>
1058
+ <div class="command"><code>Addon path: ${escapeHtml(X402_PAID_CALL_ADDON_PATH)}</code></div>
1059
+ <div class="command"><code>${escapeHtml(x402PaidCallCommand)}</code></div>
1060
+ </section>
1061
+
500
1062
  <section class="card span-8">
501
1063
  <h2>Endpoints</h2>
502
1064
  <div class="endpoint-list">
503
1065
  <div class="endpoint-row"><span class="method">POST</span><code>${escapeHtml(info.endpoints.mcp)}</code></div>
1066
+ <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.docs)}</code></div>
504
1067
  <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.health)}</code></div>
505
1068
  <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.serverInfo)}</code></div>
1069
+ <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.openApi)}</code></div>
1070
+ <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.x402Discovery)}</code></div>
1071
+ <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.payShProvider)}</code></div>
506
1072
  <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.agentCard)}</code></div>
507
1073
  <div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.wizardDescriptor)}</code></div>
508
1074
  </div>
@@ -522,6 +1088,115 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
522
1088
  </body>
523
1089
  </html>`;
524
1090
  }
1091
+ /**
1092
+ * @name buildDocsHtml
1093
+ * @description Builds the hosted SAP MCP documentation shell powered by Docsify.
1094
+ */
1095
+ export function buildDocsHtml(req, config) {
1096
+ const info = buildPublicServerInfo(req, config);
1097
+ const docsUrl = info.endpoints.docs;
1098
+ const title = 'SAP MCP Documentation | OOBE Protocol';
1099
+ 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.';
1100
+ const docsifyConfig = JSON.stringify({
1101
+ name: 'SAP MCP',
1102
+ repo: 'https://github.com/OOBE-PROTOCOL/sap-mcp',
1103
+ homepage: 'README.md',
1104
+ loadSidebar: true,
1105
+ alias: {
1106
+ '/.*/_sidebar.md': '/docs/_sidebar.md',
1107
+ },
1108
+ subMaxLevel: 2,
1109
+ auto2top: true,
1110
+ themeColor: '#28d8e8',
1111
+ search: {
1112
+ paths: 'auto',
1113
+ placeholder: 'Search SAP MCP docs',
1114
+ noData: 'No results.',
1115
+ depth: 6,
1116
+ },
1117
+ }, null, 2).replace(/</g, '\\u003c');
1118
+ return `<!doctype html>
1119
+ <html lang="en">
1120
+ <head>
1121
+ <meta charset="utf-8">
1122
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1123
+ <title>${escapeHtml(title)}</title>
1124
+ <meta name="description" content="${escapeHtml(description)}">
1125
+ <meta name="robots" content="index,follow">
1126
+ <link rel="canonical" href="${escapeHtml(docsUrl)}">
1127
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
1128
+ <link rel="icon" type="image/png" href="/favicon.png">
1129
+ <link rel="shortcut icon" href="/favicon.ico">
1130
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
1131
+ <meta property="og:type" content="website">
1132
+ <meta property="og:title" content="${escapeHtml(title)}">
1133
+ <meta property="og:description" content="${escapeHtml(description)}">
1134
+ <meta property="og:url" content="${escapeHtml(docsUrl)}">
1135
+ <meta property="og:image" content="${escapeHtml(info.endpoints.favicon)}">
1136
+ <meta name="twitter:card" content="summary">
1137
+ <meta name="twitter:title" content="${escapeHtml(title)}">
1138
+ <meta name="twitter:description" content="${escapeHtml(description)}">
1139
+ <meta name="twitter:image" content="${escapeHtml(info.endpoints.favicon)}">
1140
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsify@4.13.1/lib/themes/vue.css">
1141
+ <style>
1142
+ :root {
1143
+ --theme-color: #28d8e8;
1144
+ --sidebar-width: 310px;
1145
+ }
1146
+ body {
1147
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1148
+ color: #17242b;
1149
+ }
1150
+ .app-name-link {
1151
+ color: #062730;
1152
+ font-weight: 800;
1153
+ }
1154
+ .sidebar {
1155
+ border-right: 1px solid rgba(6,39,48,.10);
1156
+ }
1157
+ .sidebar-nav strong,
1158
+ .markdown-section h1,
1159
+ .markdown-section h2,
1160
+ .markdown-section h3 {
1161
+ color: #062730;
1162
+ letter-spacing: 0;
1163
+ }
1164
+ .markdown-section a {
1165
+ color: #087f91;
1166
+ text-underline-offset: 3px;
1167
+ }
1168
+ .markdown-section code {
1169
+ color: #062730;
1170
+ border-radius: 6px;
1171
+ }
1172
+ .markdown-section pre {
1173
+ border-radius: 8px;
1174
+ border: 1px solid rgba(6,39,48,.10);
1175
+ }
1176
+ .markdown-section table {
1177
+ display: table;
1178
+ width: 100%;
1179
+ }
1180
+ .markdown-section blockquote {
1181
+ border-left-color: #28d8e8;
1182
+ color: #49656f;
1183
+ }
1184
+ .cover-main,
1185
+ section.cover {
1186
+ background: linear-gradient(115deg, rgba(40,216,232,.13), transparent 34%), #f8fdff;
1187
+ }
1188
+ </style>
1189
+ </head>
1190
+ <body>
1191
+ <div id="app">Loading SAP MCP documentation...</div>
1192
+ <script>
1193
+ window.$docsify = ${docsifyConfig};
1194
+ </script>
1195
+ <script src="https://cdn.jsdelivr.net/npm/docsify@4.13.1/lib/docsify.min.js" defer></script>
1196
+ <script src="https://cdn.jsdelivr.net/npm/docsify@4.13.1/lib/plugins/search.min.js" defer></script>
1197
+ </body>
1198
+ </html>`;
1199
+ }
525
1200
  /**
526
1201
  * @name acceptsHtmlPreview
527
1202
  * @description Returns true for browser-style GET previews without interfering with MCP SSE clients.
@@ -662,13 +1337,16 @@ export function buildWizardInstallDescriptor(req, config) {
662
1337
  ],
663
1338
  endpoints: {
664
1339
  descriptor: `${baseUrl}/.well-known/sap-mcp-wizard.json`,
1340
+ downloads: `${baseUrl}/wizard/downloads.json`,
665
1341
  installScript: `${baseUrl}/wizard/install.sh`,
666
1342
  mcp: `${baseUrl}/mcp`,
667
1343
  },
668
1344
  commands: {
669
- runWizard: 'npm exec --yes --package @oobe-protocol-labs/sap-mcp-server -- sap-mcp-config wizard',
1345
+ runWizard: WIZARD_NPM_COMMAND,
670
1346
  showConfig: 'npm exec --yes --package @oobe-protocol-labs/sap-mcp-server -- sap-mcp-config show',
671
1347
  showProfile: 'npm exec --yes --package @oobe-protocol-labs/sap-mcp-server -- sap-mcp-config profile-info',
1348
+ installX402PaidCallAddon: WIZARD_NPM_COMMAND,
1349
+ runX402PaidCall: X402_PAID_CALL_NPX_COMMAND,
672
1350
  },
673
1351
  security: {
674
1352
  keypairBytesExposed: false,
@@ -700,6 +1378,44 @@ export function buildWizardInstallScript(req, config) {
700
1378
  '',
701
1379
  ].join('\n');
702
1380
  }
1381
+ /**
1382
+ * @name extractToolNamesFromJsonRpc
1383
+ * @description Extracts tool names from a parsed JSON-RPC 2.0 request body for request logging.
1384
+ */
1385
+ function extractToolNamesFromJsonRpc(body) {
1386
+ if (!body || typeof body !== 'object')
1387
+ return undefined;
1388
+ const record = body;
1389
+ // tools/call request: { method: "tools/call", params: { name: "tool_name", ... } }
1390
+ if (record.method === 'tools/call' && record.params && typeof record.params === 'object') {
1391
+ const params = record.params;
1392
+ if (typeof params.name === 'string')
1393
+ return [params.name];
1394
+ }
1395
+ // Batch request: array of requests
1396
+ if (Array.isArray(body)) {
1397
+ const names = [];
1398
+ for (const item of body) {
1399
+ const extracted = extractToolNamesFromJsonRpc(item);
1400
+ if (extracted)
1401
+ names.push(...extracted);
1402
+ }
1403
+ return names.length > 0 ? names : undefined;
1404
+ }
1405
+ return undefined;
1406
+ }
1407
+ /**
1408
+ * @name extractJsonRpcMethod
1409
+ * @description Extracts the JSON-RPC method name from a parsed request body for request logging.
1410
+ */
1411
+ function extractJsonRpcMethod(body) {
1412
+ if (!body || typeof body !== 'object')
1413
+ return undefined;
1414
+ const record = body;
1415
+ if (typeof record.method === 'string')
1416
+ return record.method;
1417
+ return undefined;
1418
+ }
703
1419
  /**
704
1420
  * @name RemoteMCPServer
705
1421
  * @description Remote MCP server backed by the official Streamable HTTP transport.
@@ -709,7 +1425,7 @@ export class RemoteMCPServer {
709
1425
  appConfig;
710
1426
  authManager;
711
1427
  rateLimiter;
712
- transport;
1428
+ statefulSessions = new Map();
713
1429
  httpServer;
714
1430
  constructor(config) {
715
1431
  this.appConfig = loadConfig();
@@ -730,63 +1446,92 @@ export class RemoteMCPServer {
730
1446
  this.validateAuthConfig();
731
1447
  await this.rateLimiter.initialize();
732
1448
  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
1449
  this.httpServer = http.createServer(async (req, res) => {
745
1450
  if (this.applyCors(req, res)) {
746
1451
  return;
747
1452
  }
748
1453
  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));
1454
+ if (isPublicReadMethod(req.method) && url.pathname === '/') {
1455
+ writeHtml(res, 200, buildLandingHtml(req, this.config), isHeadMethod(req.method));
751
1456
  return;
752
1457
  }
753
- if (req.method === 'GET' && url.pathname === '/server.json') {
1458
+ if (isPublicReadMethod(req.method) && ['/docs', '/docs/', '/docs/index.html'].includes(url.pathname)) {
1459
+ writeHtml(res, 200, buildDocsHtml(req, this.config), isHeadMethod(req.method));
1460
+ return;
1461
+ }
1462
+ if (isPublicReadMethod(req.method) && url.pathname === '/server.json') {
754
1463
  writeJson(res, 200, buildPublicServerInfo(req, this.config), {
755
1464
  'Cache-Control': 'public, max-age=300',
756
- });
1465
+ }, isHeadMethod(req.method));
1466
+ return;
1467
+ }
1468
+ const publicLogoAsset = resolvePublicLogoAsset(req.method, url.pathname);
1469
+ if (publicLogoAsset) {
1470
+ writeLogoAsset(res, publicLogoAsset);
757
1471
  return;
758
1472
  }
759
- if (req.method === 'GET' && ['/favicon.png', '/favicon.ico', '/apple-touch-icon.png', '/og.png'].includes(url.pathname)) {
760
- writePngAsset(res);
1473
+ const publicDocsMarkdown = resolvePublicDocsMarkdown(req.method, url.pathname);
1474
+ if (publicDocsMarkdown) {
1475
+ writeMarkdownAsset(res, publicDocsMarkdown);
761
1476
  return;
762
1477
  }
763
- if (req.method === 'GET' && url.pathname === '/health') {
1478
+ if (isPublicReadMethod(req.method) && url.pathname === '/health') {
764
1479
  writeJson(res, 200, {
765
1480
  status: 'ok',
766
1481
  service: 'sap-mcp-remote',
767
1482
  transport: 'streamable-http',
768
1483
  timestamp: new Date().toISOString(),
769
- });
1484
+ }, {}, isHeadMethod(req.method));
1485
+ return;
1486
+ }
1487
+ if (isPublicReadMethod(req.method) && url.pathname === MCP_REGISTRY_AUTH_PATH) {
1488
+ writeMcpRegistryAuthRecord(res, isHeadMethod(req.method));
1489
+ return;
1490
+ }
1491
+ if (isPublicReadMethod(req.method) && ['/.well-known/agent-card.json', '/a2a/agent-card'].includes(url.pathname)) {
1492
+ writeJson(res, 200, buildA2AAgentCard(req, this.config), {}, isHeadMethod(req.method));
1493
+ return;
1494
+ }
1495
+ if (isPublicReadMethod(req.method) && ['/.well-known/sap-mcp-wizard.json', '/wizard', '/wizard.json'].includes(url.pathname)) {
1496
+ writeJson(res, 200, buildWizardInstallDescriptor(req, this.config), {}, isHeadMethod(req.method));
770
1497
  return;
771
1498
  }
772
- if (req.method === 'GET' && ['/.well-known/agent-card.json', '/a2a/agent-card'].includes(url.pathname)) {
773
- writeJson(res, 200, buildA2AAgentCard(req, this.config));
1499
+ if (isPublicReadMethod(req.method) && url.pathname === '/wizard/downloads.json') {
1500
+ const info = buildPublicServerInfo(req, this.config);
1501
+ writeJson(res, 200, info.downloads, {
1502
+ 'Cache-Control': 'public, max-age=300',
1503
+ }, isHeadMethod(req.method));
1504
+ return;
1505
+ }
1506
+ if (isPublicReadMethod(req.method) && url.pathname === '/openapi.json') {
1507
+ writeJson(res, 200, buildOpenApiSpec(req, this.config), {
1508
+ 'Cache-Control': 'public, max-age=300',
1509
+ }, isHeadMethod(req.method));
774
1510
  return;
775
1511
  }
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));
1512
+ if (isPublicReadMethod(req.method) && url.pathname === '/pay/provider.yml') {
1513
+ const providerYaml = buildPublicPayShProviderYaml(req, this.config, this.appConfig);
1514
+ writeText(res, 200, providerYaml, 'application/yaml; charset=utf-8', {}, isHeadMethod(req.method));
778
1515
  return;
779
1516
  }
780
- if (req.method === 'GET' && url.pathname === '/wizard/install.sh') {
1517
+ if (isPublicReadMethod(req.method) && url.pathname === '/.well-known/x402') {
1518
+ writeJson(res, 200, buildX402DiscoveryDocument(req, this.config), {
1519
+ 'Cache-Control': 'public, max-age=300',
1520
+ }, isHeadMethod(req.method));
1521
+ return;
1522
+ }
1523
+ if (isPublicReadMethod(req.method) && url.pathname === '/wizard/install.sh') {
1524
+ const script = buildWizardInstallScript(req, this.config);
781
1525
  res.writeHead(200, {
782
1526
  'Content-Type': 'text/x-shellscript; charset=utf-8',
783
1527
  'Cache-Control': 'public, max-age=300',
1528
+ 'Content-Length': Buffer.byteLength(script),
784
1529
  });
785
- res.end(buildWizardInstallScript(req, this.config));
1530
+ res.end(isHeadMethod(req.method) ? undefined : script);
786
1531
  return;
787
1532
  }
788
- if (req.method === 'GET' && url.pathname === '/mcp' && acceptsHtmlPreview(req)) {
789
- writeHtml(res, 200, buildLandingHtml(req, this.config, 'mcp'));
1533
+ if (isPublicReadMethod(req.method) && url.pathname === '/mcp' && (isHeadMethod(req.method) || acceptsHtmlPreview(req))) {
1534
+ writeHtml(res, 200, buildLandingHtml(req, this.config, 'mcp'), isHeadMethod(req.method));
790
1535
  return;
791
1536
  }
792
1537
  if (url.pathname !== '/mcp') {
@@ -813,6 +1558,26 @@ export class RemoteMCPServer {
813
1558
  });
814
1559
  return;
815
1560
  }
1561
+ // ── Request logging ─────────────────────────────────────────────
1562
+ const reqStart = Date.now();
1563
+ const callerIp = getRateLimitKey(req);
1564
+ const originalEnd = res.end.bind(res);
1565
+ let loggedMethod;
1566
+ let loggedToolNames;
1567
+ res.end = ((...args) => {
1568
+ const latencyMs = Date.now() - reqStart;
1569
+ const statusCode = res.statusCode;
1570
+ logger.info('MCP request', {
1571
+ method: req.method,
1572
+ path: url.pathname,
1573
+ status: statusCode,
1574
+ latencyMs,
1575
+ callerIp,
1576
+ toolNames: loggedToolNames,
1577
+ rpcMethod: loggedMethod,
1578
+ });
1579
+ return originalEnd(...args);
1580
+ });
816
1581
  try {
817
1582
  if (this.config.stateless) {
818
1583
  // Stateless mode: create a fresh transport + server per request.
@@ -824,22 +1589,32 @@ export class RemoteMCPServer {
824
1589
  await perRequestServer.connect(perRequestTransport);
825
1590
  if (monetizationGate) {
826
1591
  await monetizationGate.handle(req, res, async (mcpReq, mcpRes, parsedBody) => {
1592
+ // Extract tool names from parsed JSON-RPC body for logging
1593
+ loggedToolNames = extractToolNamesFromJsonRpc(parsedBody);
1594
+ loggedMethod = extractJsonRpcMethod(parsedBody);
827
1595
  await perRequestTransport.handleRequest(mcpReq, mcpRes, parsedBody);
828
1596
  });
829
1597
  }
830
1598
  else {
1599
+ // Parse body for logging before passing to transport
1600
+ // The transport will re-read it from the stream
1601
+ loggedToolNames = undefined; // Will be logged without tool names in free mode
831
1602
  await perRequestTransport.handleRequest(req, res);
832
1603
  }
833
1604
  }
834
1605
  else {
835
- // Stateful mode: reuse the long-lived transport.
1606
+ // Stateful mode: route each request to the transport created by its initialize response.
836
1607
  if (monetizationGate) {
837
1608
  await monetizationGate.handle(req, res, async (mcpReq, mcpRes, parsedBody) => {
838
- await this.transport?.handleRequest(mcpReq, mcpRes, parsedBody);
1609
+ loggedToolNames = extractToolNamesFromJsonRpc(parsedBody);
1610
+ loggedMethod = extractJsonRpcMethod(parsedBody);
1611
+ await this.handleStatefulMcpRequest(mcpReq, mcpRes, parsedBody);
1612
+ }, {
1613
+ validatePaidRequest: context => this.validateStatefulPaidRequest(context.request, context.parsedBody),
839
1614
  });
840
1615
  }
841
1616
  else {
842
- await this.transport?.handleRequest(req, res);
1617
+ await this.handleStatefulMcpRequest(req, res);
843
1618
  }
844
1619
  }
845
1620
  }
@@ -878,7 +1653,9 @@ export class RemoteMCPServer {
878
1653
  * @description Gracefully closes the MCP transport and HTTP listener.
879
1654
  */
880
1655
  async stop() {
881
- await this.transport?.close();
1656
+ const sessions = Array.from(this.statefulSessions.values());
1657
+ this.statefulSessions.clear();
1658
+ await Promise.allSettled(sessions.map(session => session.transport.close()));
882
1659
  if (!this.httpServer) {
883
1660
  return;
884
1661
  }
@@ -906,6 +1683,110 @@ export class RemoteMCPServer {
906
1683
  throw new Error('Remote MCP JWT auth requires SAP_MCP_AUTH_SECRET');
907
1684
  }
908
1685
  }
1686
+ /**
1687
+ * @name handleStatefulMcpRequest
1688
+ * @description Routes a stateful MCP request to the session transport created by initialize.
1689
+ */
1690
+ async handleStatefulMcpRequest(req, res, parsedBody) {
1691
+ const transport = await this.resolveStatefulTransport(req, parsedBody);
1692
+ if (!transport) {
1693
+ const providedSessionId = readSingleHeader(req.headers['mcp-session-id']);
1694
+ writeMcpJsonRpcError(res, providedSessionId ? 404 : 400, providedSessionId ? -32011 : -32010, extractJsonRpcId(parsedBody), providedSessionId ? 'mcp_session_not_found' : 'mcp_session_required', {
1695
+ requiredHeader: 'mcp-session-id',
1696
+ ...(providedSessionId ? { providedSessionId } : {}),
1697
+ hint: providedSessionId
1698
+ ? 'The mcp-session-id must come from this server initialize response. Client-generated UUIDs are not valid MCP sessions.'
1699
+ : 'Call initialize first and reuse the returned mcp-session-id header. Do not generate a client-side UUID.',
1700
+ });
1701
+ return;
1702
+ }
1703
+ await transport.handleRequest(req, res, parsedBody);
1704
+ }
1705
+ /**
1706
+ * @name validateStatefulPaidRequest
1707
+ * @description Fails paid tool calls before x402 verification when the MCP session is missing or unknown.
1708
+ */
1709
+ validateStatefulPaidRequest(req, parsedBody) {
1710
+ if (isInitializeJsonRpcRequest(parsedBody)) {
1711
+ return undefined;
1712
+ }
1713
+ const sessionId = readSingleHeader(req.headers['mcp-session-id']);
1714
+ if (!sessionId) {
1715
+ return {
1716
+ code: -32010,
1717
+ message: 'mcp_session_required',
1718
+ data: {
1719
+ requiredHeader: 'mcp-session-id',
1720
+ hint: 'Initialize the MCP Streamable HTTP session first, then reuse the returned mcp-session-id for the unpaid payment challenge and the paid retry.',
1721
+ },
1722
+ };
1723
+ }
1724
+ if (!this.statefulSessions.has(sessionId)) {
1725
+ return {
1726
+ code: -32011,
1727
+ message: 'mcp_session_not_found',
1728
+ data: {
1729
+ requiredHeader: 'mcp-session-id',
1730
+ providedSessionId: sessionId,
1731
+ hint: 'The mcp-session-id must come from this server initialize response. Client-generated UUIDs are not valid MCP sessions.',
1732
+ },
1733
+ };
1734
+ }
1735
+ return undefined;
1736
+ }
1737
+ /**
1738
+ * @name resolveStatefulTransport
1739
+ * @description Returns the transport for an existing session or creates one for an initialize request.
1740
+ */
1741
+ async resolveStatefulTransport(req, parsedBody) {
1742
+ const sessionId = readSingleHeader(req.headers['mcp-session-id']);
1743
+ if (sessionId) {
1744
+ const session = this.statefulSessions.get(sessionId);
1745
+ if (!session) {
1746
+ return undefined;
1747
+ }
1748
+ session.lastSeenAt = Date.now();
1749
+ return session.transport;
1750
+ }
1751
+ if (req.method === 'POST' && (parsedBody === undefined || isInitializeJsonRpcRequest(parsedBody))) {
1752
+ return await this.createStatefulSessionTransport();
1753
+ }
1754
+ return undefined;
1755
+ }
1756
+ /**
1757
+ * @name createStatefulSessionTransport
1758
+ * @description Creates one MCP server and Streamable HTTP transport for a single initialized session.
1759
+ */
1760
+ async createStatefulSessionTransport() {
1761
+ const server = await createSapMcpServer(this.appConfig);
1762
+ const transport = new StreamableHTTPServerTransport({
1763
+ sessionIdGenerator: () => randomUUID(),
1764
+ onsessioninitialized: sessionId => {
1765
+ this.statefulSessions.set(sessionId, {
1766
+ server,
1767
+ transport,
1768
+ createdAt: Date.now(),
1769
+ lastSeenAt: Date.now(),
1770
+ });
1771
+ logger.info('Stateful MCP session initialized', {
1772
+ sessionId,
1773
+ activeSessions: this.statefulSessions.size,
1774
+ });
1775
+ },
1776
+ });
1777
+ transport.onclose = () => {
1778
+ const sessionId = transport.sessionId;
1779
+ if (sessionId) {
1780
+ this.statefulSessions.delete(sessionId);
1781
+ logger.info('Stateful MCP session closed', {
1782
+ sessionId,
1783
+ activeSessions: this.statefulSessions.size,
1784
+ });
1785
+ }
1786
+ };
1787
+ await server.connect(transport);
1788
+ return transport;
1789
+ }
909
1790
  /**
910
1791
  * @name applyCors
911
1792
  * @description Applies configured CORS headers and handles preflight requests.