@oobe-protocol-labs/sap-mcp-server 0.8.0 → 0.9.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 (133) hide show
  1. package/.env.example +30 -0
  2. package/CHANGELOG.md +59 -1
  3. package/README.md +19 -2
  4. package/USER_DOCS/00_START_HERE.md +1 -0
  5. package/USER_DOCS/01_HOSTED_REMOTE_MCP.md +17 -3
  6. package/USER_DOCS/03_PAYMENTS_X402_PAYSH.md +11 -1
  7. package/USER_DOCS/04_CLIENT_CONFIGS.md +4 -3
  8. package/USER_DOCS/05_SKILLS_AND_TOOLS.md +27 -3
  9. package/USER_DOCS/06_DESKTOP_GUI_WIZARD.md +14 -7
  10. package/USER_DOCS/07_SMITHERY_AND_MARKETPLACES.md +163 -0
  11. package/assets/logos/claude.png +0 -0
  12. package/assets/logos/codex.webp +0 -0
  13. package/assets/logos/hermes.png +0 -0
  14. package/assets/logos/openclaw.svg +22 -0
  15. package/config.schema.json +5 -0
  16. package/dist/config/env.d.ts +4 -0
  17. package/dist/config/env.d.ts.map +1 -1
  18. package/dist/config/env.js +3 -0
  19. package/dist/config/env.js.map +1 -1
  20. package/dist/config/mcp-client-injection.js +2 -2
  21. package/dist/config/mcp-client-injection.js.map +1 -1
  22. package/dist/config/paths.d.ts +6 -0
  23. package/dist/config/paths.d.ts.map +1 -1
  24. package/dist/config/paths.js +14 -0
  25. package/dist/config/paths.js.map +1 -1
  26. package/dist/config/wizard.js +3 -3
  27. package/dist/config/wizard.js.map +1 -1
  28. package/dist/core/constants.d.ts +1 -1
  29. package/dist/core/constants.js +1 -1
  30. package/dist/payments/pricing.d.ts +1 -1
  31. package/dist/payments/pricing.d.ts.map +1 -1
  32. package/dist/payments/pricing.js +21 -3
  33. package/dist/payments/pricing.js.map +1 -1
  34. package/dist/payments/x402-paid-call.d.ts +96 -0
  35. package/dist/payments/x402-paid-call.d.ts.map +1 -1
  36. package/dist/payments/x402-paid-call.js +264 -2
  37. package/dist/payments/x402-paid-call.js.map +1 -1
  38. package/dist/policy/local-policy-engine.js +1 -1
  39. package/dist/policy/local-policy-engine.js.map +1 -1
  40. package/dist/prompts/context/sap-agent-context.prompt.js +8 -1
  41. package/dist/prompts/context/sap-agent-context.prompt.js.map +1 -1
  42. package/dist/prompts/context/sap-agent-start.prompt.d.ts +14 -0
  43. package/dist/prompts/context/sap-agent-start.prompt.d.ts.map +1 -0
  44. package/dist/prompts/context/sap-agent-start.prompt.js +96 -0
  45. package/dist/prompts/context/sap-agent-start.prompt.js.map +1 -0
  46. package/dist/prompts/developer/generate-sap-integration.prompt.js +1 -1
  47. package/dist/prompts/developer/generate-sap-integration.prompt.js.map +1 -1
  48. package/dist/prompts/register-prompts.d.ts.map +1 -1
  49. package/dist/prompts/register-prompts.js +3 -1
  50. package/dist/prompts/register-prompts.js.map +1 -1
  51. package/dist/remote/public-home/index.d.ts.map +1 -1
  52. package/dist/remote/public-home/index.js +2 -1
  53. package/dist/remote/public-home/index.js.map +1 -1
  54. package/dist/remote/public-home/scripts.d.ts +1 -1
  55. package/dist/remote/public-home/scripts.d.ts.map +1 -1
  56. package/dist/remote/public-home/scripts.js +29 -0
  57. package/dist/remote/public-home/scripts.js.map +1 -1
  58. package/dist/remote/public-home/sections.d.ts +5 -0
  59. package/dist/remote/public-home/sections.d.ts.map +1 -1
  60. package/dist/remote/public-home/sections.js +76 -4
  61. package/dist/remote/public-home/sections.js.map +1 -1
  62. package/dist/remote/public-home/styles.d.ts +1 -1
  63. package/dist/remote/public-home/styles.d.ts.map +1 -1
  64. package/dist/remote/public-home/styles.js +460 -16
  65. package/dist/remote/public-home/styles.js.map +1 -1
  66. package/dist/remote/public-home/types.d.ts +2 -0
  67. package/dist/remote/public-home/types.d.ts.map +1 -1
  68. package/dist/remote/rate-limiter.d.ts +2 -0
  69. package/dist/remote/rate-limiter.d.ts.map +1 -1
  70. package/dist/remote/rate-limiter.js +23 -0
  71. package/dist/remote/rate-limiter.js.map +1 -1
  72. package/dist/remote/server.d.ts +72 -1
  73. package/dist/remote/server.d.ts.map +1 -1
  74. package/dist/remote/server.js +424 -15
  75. package/dist/remote/server.js.map +1 -1
  76. package/dist/runtime/module-resolution.d.ts.map +1 -1
  77. package/dist/runtime/module-resolution.js +41 -8
  78. package/dist/runtime/module-resolution.js.map +1 -1
  79. package/dist/schemas/settlement.schema.d.ts +2 -2
  80. package/dist/security/tool-permissions.d.ts.map +1 -1
  81. package/dist/security/tool-permissions.js +1 -6
  82. package/dist/security/tool-permissions.js.map +1 -1
  83. package/dist/tools/agent-start-tool.d.ts +12 -0
  84. package/dist/tools/agent-start-tool.d.ts.map +1 -0
  85. package/dist/tools/agent-start-tool.js +156 -0
  86. package/dist/tools/agent-start-tool.js.map +1 -0
  87. package/dist/tools/client-sdk-tools.d.ts +7 -0
  88. package/dist/tools/client-sdk-tools.d.ts.map +1 -1
  89. package/dist/tools/client-sdk-tools.js +80 -4
  90. package/dist/tools/client-sdk-tools.js.map +1 -1
  91. package/dist/tools/index.d.ts +1 -0
  92. package/dist/tools/index.d.ts.map +1 -1
  93. package/dist/tools/index.js +3 -1
  94. package/dist/tools/index.js.map +1 -1
  95. package/dist/tools/register-tools.d.ts.map +1 -1
  96. package/dist/tools/register-tools.js +5 -1
  97. package/dist/tools/register-tools.js.map +1 -1
  98. package/dist/tools/sap-sdk-tools.d.ts +1 -1
  99. package/dist/tools/sap-sdk-tools.d.ts.map +1 -1
  100. package/dist/tools/sap-sdk-tools.js +108 -105
  101. package/dist/tools/sap-sdk-tools.js.map +1 -1
  102. package/dist/tools/sap-sns-tools.d.ts +4 -4
  103. package/dist/tools/sap-sns-tools.js +4 -4
  104. package/dist/tools/skills-tools.js +1 -1
  105. package/dist/tools/skills-tools.js.map +1 -1
  106. package/dist/tools/x402-paid-call-tool.d.ts.map +1 -1
  107. package/dist/tools/x402-paid-call-tool.js +50 -4
  108. package/dist/tools/x402-paid-call-tool.js.map +1 -1
  109. package/dist/tui/config-wizard.js +2 -2
  110. package/dist/tui/config-wizard.js.map +1 -1
  111. package/dist/tui/wizard-save.d.ts.map +1 -1
  112. package/dist/tui/wizard-save.js +7 -3
  113. package/dist/tui/wizard-save.js.map +1 -1
  114. package/dist/wizard-core/desktop-flow.d.ts +1 -1
  115. package/dist/wizard-core/desktop-flow.d.ts.map +1 -1
  116. package/dist/wizard-core/desktop-flow.js +6 -5
  117. package/dist/wizard-core/desktop-flow.js.map +1 -1
  118. package/docs/03_CONFIGURATION_AND_WIZARD.md +1 -0
  119. package/docs/05_REMOTE_VPS_DEPLOYMENT.md +32 -0
  120. package/docs/06_PAYMENTS_X402_AND_PAYSH.md +12 -0
  121. package/docs/07_ENDPOINTS_AND_CLIENTS.md +1 -1
  122. package/docs/09_TOOLS_SKILLS_AND_AGENT_GUIDE.md +42 -11
  123. package/docs/14_DESKTOP_WIZARD_RELEASE.md +2 -1
  124. package/docs/README.md +6 -3
  125. package/docs/_sidebar.md +1 -0
  126. package/package.json +3 -2
  127. package/server.json +3 -3
  128. package/skills/README.md +2 -2
  129. package/skills/sap-escrow-settlement/SKILL.md +15 -11
  130. package/skills/sap-mcp/SKILL.md +39 -20
  131. package/skills/sap-mcp/TOOL_REFERENCE.md +6 -5
  132. package/skills/sap-operations/SKILL.md +6 -7
  133. package/smithery.config.schema.json +59 -0
@@ -25,6 +25,7 @@ const OOBE_LOGO_ASSET_PATH = join(dirname(fileURLToPath(import.meta.url)), '..',
25
25
  const LOGO_ASSET_ROOT_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'assets', 'logos');
26
26
  const DOCS_ROOT_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'docs');
27
27
  const USER_DOCS_ROOT_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'USER_DOCS');
28
+ const SMITHERY_CONFIG_SCHEMA_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'smithery.config.schema.json');
28
29
  const PAYMENT_STATS_CACHE_MS = 15_000;
29
30
  const SERVER_CARD_CACHE_MS = 300_000;
30
31
  const RELEASE_DOWNLOAD_BASE_URL = `https://github.com/OOBE-PROTOCOL/sap-mcp/releases/download/${MCP_SERVER_VERSION}`;
@@ -40,14 +41,51 @@ let oobeLogoAssetCache;
40
41
  let paymentStatsCache;
41
42
  let serverCardCapabilitiesCache;
42
43
  const PUBLIC_LOGO_ASSETS = {
44
+ '/logos/claude.png': { filename: 'claude.png', contentType: 'image/png' },
45
+ '/logos/codex.webp': { filename: 'codex.webp', contentType: 'image/webp' },
43
46
  '/logos/drift.svg': { filename: 'drift.svg', contentType: 'image/svg+xml' },
47
+ '/logos/hermes.png': { filename: 'hermes.png', contentType: 'image/png' },
44
48
  '/logos/jupiter.ico': { filename: 'jupiter.ico', contentType: 'image/x-icon' },
45
49
  '/logos/mcp.svg': { filename: 'mcp.svg', contentType: 'image/svg+xml' },
46
50
  '/logos/meteora.png': { filename: 'meteora.png', contentType: 'image/png' },
47
51
  '/logos/orca.ico': { filename: 'orca.ico', contentType: 'image/x-icon' },
52
+ '/logos/openclaw.svg': { filename: 'openclaw.svg', contentType: 'image/svg+xml' },
48
53
  '/logos/raydium.ico': { filename: 'raydium.ico', contentType: 'image/x-icon' },
49
54
  '/logos/smithery.svg': { filename: 'smithery.svg', contentType: 'image/svg+xml' },
50
55
  };
56
+ class RemoteCapacityError extends Error {
57
+ constructor(message) {
58
+ super(message);
59
+ this.name = 'RemoteCapacityError';
60
+ }
61
+ }
62
+ class InFlightRequestLimiter {
63
+ maxInFlightRequests;
64
+ active = 0;
65
+ constructor(maxInFlightRequests) {
66
+ this.maxInFlightRequests = maxInFlightRequests;
67
+ }
68
+ tryAcquire() {
69
+ if (this.maxInFlightRequests <= 0) {
70
+ return () => undefined;
71
+ }
72
+ if (this.active >= this.maxInFlightRequests) {
73
+ return undefined;
74
+ }
75
+ this.active += 1;
76
+ let released = false;
77
+ return () => {
78
+ if (released) {
79
+ return;
80
+ }
81
+ released = true;
82
+ this.active = Math.max(0, this.active - 1);
83
+ };
84
+ }
85
+ getActiveCount() {
86
+ return this.active;
87
+ }
88
+ }
51
89
  /**
52
90
  * @name parseApiKeys
53
91
  * @description Parses comma-separated `apiKey=userId` entries from environment configuration.
@@ -81,6 +119,9 @@ export function defaultRemoteConfig(appConfig) {
81
119
  corsOrigins: appConfig.httpCorsOrigins,
82
120
  stateless: parseRemoteBoolean(process.env.SAP_MCP_HTTP_STATELESS, false),
83
121
  rateLimit: buildRemoteRateLimitConfigFromEnv(appConfig.rateLimitPerMinute),
122
+ sessions: buildRemoteSessionConfigFromEnv(),
123
+ concurrency: buildRemoteConcurrencyConfigFromEnv(),
124
+ http: buildRemoteHttpTuningConfigFromEnv(),
84
125
  paymentDiscovery: buildPublicPaymentDiscovery(appConfig),
85
126
  paymentPriceRange: {
86
127
  minUsd: appConfig.monetization.prices.minUsd,
@@ -173,6 +214,34 @@ function parseRemoteBoolean(raw, fallback) {
173
214
  }
174
215
  return ['true', '1', 'yes', 'on'].includes(raw.trim().toLowerCase());
175
216
  }
217
+ function buildRemoteSessionConfigFromEnv() {
218
+ return {
219
+ maxStatefulSessions: parseRemotePositiveInteger(process.env.SAP_MCP_REMOTE_MAX_STATEFUL_SESSIONS, 2_000),
220
+ idleTtlMs: parseRemotePositiveInteger(process.env.SAP_MCP_REMOTE_SESSION_IDLE_TTL_MS, 10 * 60 * 1000),
221
+ absoluteTtlMs: parseRemotePositiveInteger(process.env.SAP_MCP_REMOTE_SESSION_ABSOLUTE_TTL_MS, 60 * 60 * 1000),
222
+ cleanupIntervalMs: parseRemotePositiveInteger(process.env.SAP_MCP_REMOTE_SESSION_CLEANUP_INTERVAL_MS, 30_000),
223
+ };
224
+ }
225
+ function buildRemoteConcurrencyConfigFromEnv() {
226
+ return {
227
+ maxInFlightRequests: parseRemotePositiveInteger(process.env.SAP_MCP_REMOTE_MAX_IN_FLIGHT_REQUESTS, 1_024),
228
+ };
229
+ }
230
+ function buildRemoteHttpTuningConfigFromEnv() {
231
+ return {
232
+ requestTimeoutMs: parseRemotePositiveInteger(process.env.SAP_MCP_REMOTE_REQUEST_TIMEOUT_MS, 5 * 60 * 1000),
233
+ headersTimeoutMs: parseRemotePositiveInteger(process.env.SAP_MCP_REMOTE_HEADERS_TIMEOUT_MS, 65_000),
234
+ keepAliveTimeoutMs: parseRemotePositiveInteger(process.env.SAP_MCP_REMOTE_KEEP_ALIVE_TIMEOUT_MS, 75_000),
235
+ maxRequestsPerSocket: parseRemotePositiveInteger(process.env.SAP_MCP_REMOTE_MAX_REQUESTS_PER_SOCKET, 0),
236
+ };
237
+ }
238
+ function parseRemotePositiveInteger(raw, fallback) {
239
+ if (!raw) {
240
+ return fallback;
241
+ }
242
+ const value = Number.parseInt(raw, 10);
243
+ return Number.isFinite(value) && value > 0 ? value : fallback;
244
+ }
176
245
  /**
177
246
  * @name writeJson
178
247
  * @description Writes a JSON response with optional additional headers.
@@ -475,6 +544,8 @@ function readPaymentStatsFromLedger() {
475
544
  totalPaymentRequests: 0,
476
545
  totalVerifiedPayments: 0,
477
546
  totalFailedSettlements: 0,
547
+ uniqueRemoteCallers: 0,
548
+ uniqueUserAgents: 0,
478
549
  ledgerAvailable: false,
479
550
  };
480
551
  const ledgerPath = join(getDataDir(), 'payments', 'usage-ledger.jsonl');
@@ -482,12 +553,20 @@ function readPaymentStatsFromLedger() {
482
553
  return stats;
483
554
  }
484
555
  stats.ledgerAvailable = true;
556
+ const remoteCallers = new Set();
557
+ const userAgents = new Set();
485
558
  const lines = readFileSync(ledgerPath, 'utf-8').split('\n');
486
559
  for (const line of lines) {
487
560
  const event = parsePaymentLedgerEvent(line);
488
561
  if (!event) {
489
562
  continue;
490
563
  }
564
+ if (event.remoteAddress) {
565
+ remoteCallers.add(event.remoteAddress);
566
+ }
567
+ if (event.userAgent) {
568
+ userAgents.add(event.userAgent);
569
+ }
491
570
  if (event.event === 'payment_required') {
492
571
  stats.totalPaymentRequests += 1;
493
572
  }
@@ -503,6 +582,8 @@ function readPaymentStatsFromLedger() {
503
582
  stats.totalFailedSettlements += 1;
504
583
  }
505
584
  }
585
+ stats.uniqueRemoteCallers = remoteCallers.size;
586
+ stats.uniqueUserAgents = userAgents.size;
506
587
  return stats;
507
588
  }
508
589
  function parsePaymentLedgerEvent(line) {
@@ -607,6 +688,7 @@ export function buildPublicServerInfo(req, config) {
607
688
  serverInfo: `${baseUrl}/server.json`,
608
689
  openApi: `${baseUrl}/openapi.json`,
609
690
  x402Discovery: `${baseUrl}/.well-known/x402`,
691
+ smitheryConfigSchema: `${baseUrl}/smithery.config.schema.json`,
610
692
  smitheryServerCard: `${baseUrl}/.well-known/mcp/server-card.json`,
611
693
  payShProvider: `${baseUrl}/pay/provider.yml`,
612
694
  agentCard: `${baseUrl}/.well-known/agent-card.json`,
@@ -650,6 +732,34 @@ export function buildPublicServerInfo(req, config) {
650
732
  },
651
733
  };
652
734
  }
735
+ /**
736
+ * @name buildMarketplaceConfigurationMetadata
737
+ * @description Builds public Smithery/marketplace configuration hints for non-custodial x402 usage.
738
+ */
739
+ export function buildMarketplaceConfigurationMetadata(req, config) {
740
+ const baseUrl = buildPublicBaseUrl(req, config);
741
+ return {
742
+ required: false,
743
+ schemaUrl: `${baseUrl}/smithery.config.schema.json`,
744
+ configSchema: readSmitheryConfigSchema(),
745
+ recommendedMode: 'free-discovery',
746
+ paidToolModes: ['x402-native-client', 'local-sap-payments-bridge'],
747
+ instructions: 'Start with free discovery tools in Smithery. Paid/write tools return x402 challenges; complete them with a native x402-capable client or with the local sap_payments bridge installed by the SAP MCP wizard.',
748
+ setupCommand: WIZARD_NPM_COMMAND,
749
+ localBridge: {
750
+ serverName: 'sap_payments',
751
+ readinessTool: 'sap_payments_readiness',
752
+ paidCallTool: 'sap_payments_call_paid_tool',
753
+ },
754
+ };
755
+ }
756
+ /**
757
+ * @name readSmitheryConfigSchema
758
+ * @description Reads the published Smithery JSON schema from the packaged root.
759
+ */
760
+ export function readSmitheryConfigSchema() {
761
+ return JSON.parse(readFileSync(SMITHERY_CONFIG_SCHEMA_PATH, 'utf-8'));
762
+ }
653
763
  /**
654
764
  * @name getStaticServerCardCapabilities
655
765
  * @description Reads capabilities from the real MCP registration store so static metadata never drifts from tools/list.
@@ -727,6 +837,7 @@ export async function buildStaticServerCard(req, config, appConfig) {
727
837
  required: authRequired,
728
838
  schemes: authRequired ? ['Bearer'] : ['none', 'x402'],
729
839
  },
840
+ configuration: buildMarketplaceConfigurationMetadata(req, config),
730
841
  transport: {
731
842
  type: 'streamable-http',
732
843
  url: `${baseUrl}/mcp`,
@@ -1046,52 +1157,219 @@ export function buildDocsHtml(req, config) {
1046
1157
  :root {
1047
1158
  --theme-color: #28d8e8;
1048
1159
  --sidebar-width: 310px;
1160
+ --sap-bg: #111413;
1161
+ --sap-panel: rgba(255, 255, 255, .055);
1162
+ --sap-line: rgba(255, 255, 255, .14);
1163
+ --sap-aqua: #28d8e8;
1164
+ --sap-ink: #f5fbfc;
1165
+ --sap-muted: #a8b9bc;
1166
+ }
1167
+ * {
1168
+ box-sizing: border-box;
1169
+ }
1170
+ html {
1171
+ background: var(--sap-bg);
1049
1172
  }
1050
1173
  body {
1051
1174
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1052
- color: #17242b;
1175
+ color: var(--sap-ink);
1176
+ background:
1177
+ linear-gradient(rgba(40, 216, 232, .025) 1px, transparent 1px),
1178
+ linear-gradient(90deg, rgba(40, 216, 232, .025) 1px, transparent 1px),
1179
+ var(--sap-bg);
1180
+ background-size: 80px 80px;
1181
+ overflow-x: hidden;
1182
+ }
1183
+ body::before {
1184
+ content: "";
1185
+ position: fixed;
1186
+ inset: 0;
1187
+ z-index: -1;
1188
+ pointer-events: none;
1189
+ background:
1190
+ radial-gradient(circle at 16% 10%, rgba(40, 216, 232, .12), transparent 30%),
1191
+ radial-gradient(circle at 84% 18%, rgba(134, 166, 255, .08), transparent 28%),
1192
+ linear-gradient(180deg, rgba(255,255,255,.025), transparent 40%);
1193
+ }
1194
+ .docs-home {
1195
+ position: fixed;
1196
+ top: 14px;
1197
+ left: 14px;
1198
+ z-index: 80;
1199
+ display: inline-flex;
1200
+ align-items: center;
1201
+ gap: 10px;
1202
+ min-height: 54px;
1203
+ border: 1px solid rgba(255,255,255,.12);
1204
+ border-radius: 999px;
1205
+ padding: 7px 15px 7px 8px;
1206
+ color: var(--sap-ink);
1207
+ text-decoration: none;
1208
+ font-weight: 850;
1209
+ background: rgba(11, 17, 17, .82);
1210
+ box-shadow: 0 18px 44px rgba(0, 0, 0, .28);
1211
+ backdrop-filter: blur(18px);
1212
+ -webkit-backdrop-filter: blur(18px);
1213
+ }
1214
+ .docs-home:hover,
1215
+ .docs-home:focus-visible {
1216
+ border-color: rgba(40,216,232,.44);
1217
+ background: rgba(40,216,232,.09);
1218
+ }
1219
+ .docs-home img {
1220
+ width: 38px;
1221
+ height: 38px;
1222
+ border-radius: 12px;
1223
+ box-shadow: 0 0 0 1px rgba(40,216,232,.2);
1224
+ }
1225
+ .docs-home span {
1226
+ display: grid;
1227
+ gap: 1px;
1228
+ line-height: 1.05;
1229
+ }
1230
+ .docs-home small {
1231
+ color: var(--sap-muted);
1232
+ font-size: 11px;
1233
+ font-weight: 750;
1234
+ }
1235
+ .app-nav {
1236
+ background: rgba(11, 17, 17, .9);
1237
+ border-bottom: 1px solid var(--sap-line);
1238
+ color: var(--sap-ink);
1239
+ backdrop-filter: blur(16px);
1240
+ -webkit-backdrop-filter: blur(16px);
1241
+ }
1242
+ .github-corner svg {
1243
+ color: var(--sap-bg);
1244
+ fill: var(--sap-aqua);
1245
+ }
1246
+ .search input {
1247
+ border: 1px solid rgba(255,255,255,.12);
1248
+ border-radius: 999px;
1249
+ color: var(--sap-ink);
1250
+ background: rgba(255,255,255,.055);
1053
1251
  }
1054
1252
  .app-name-link {
1055
- color: #062730;
1253
+ color: var(--sap-ink);
1056
1254
  font-weight: 800;
1057
1255
  }
1256
+ .app-name {
1257
+ margin-top: 82px;
1258
+ }
1058
1259
  .sidebar {
1059
- border-right: 1px solid rgba(6,39,48,.10);
1260
+ border-right: 1px solid var(--sap-line);
1261
+ background:
1262
+ linear-gradient(180deg, rgba(255,255,255,.055), rgba(255,255,255,.028)),
1263
+ rgba(9, 17, 18, .92);
1264
+ color: var(--sap-muted);
1265
+ }
1266
+ .sidebar-toggle {
1267
+ background: rgba(11, 17, 17, .88);
1268
+ border-radius: 0 14px 14px 0;
1269
+ border: 1px solid var(--sap-line);
1270
+ border-left: 0;
1060
1271
  }
1061
1272
  .sidebar-nav strong,
1062
1273
  .markdown-section h1,
1063
1274
  .markdown-section h2,
1064
1275
  .markdown-section h3 {
1065
- color: #062730;
1276
+ color: var(--sap-ink);
1066
1277
  letter-spacing: 0;
1067
1278
  }
1279
+ .sidebar ul li a,
1280
+ .sidebar ul li.active > a,
1281
+ .anchor span {
1282
+ color: var(--sap-muted);
1283
+ }
1284
+ .sidebar ul li.active > a,
1285
+ .sidebar ul li a:hover {
1286
+ color: var(--sap-aqua);
1287
+ }
1288
+ .markdown-section {
1289
+ max-width: 920px;
1290
+ color: var(--sap-muted);
1291
+ }
1292
+ .markdown-section strong,
1293
+ .markdown-section table,
1294
+ .markdown-section li {
1295
+ color: var(--sap-muted);
1296
+ }
1068
1297
  .markdown-section a {
1069
- color: #087f91;
1298
+ color: var(--sap-aqua);
1070
1299
  text-underline-offset: 3px;
1071
1300
  }
1072
1301
  .markdown-section code {
1073
- color: #062730;
1302
+ color: #d8faff;
1074
1303
  border-radius: 6px;
1304
+ background: rgba(255,255,255,.08);
1075
1305
  }
1076
1306
  .markdown-section pre {
1077
1307
  border-radius: 8px;
1078
- border: 1px solid rgba(6,39,48,.10);
1308
+ border: 1px solid var(--sap-line);
1309
+ background: rgba(0,0,0,.34);
1079
1310
  }
1080
1311
  .markdown-section table {
1081
1312
  display: table;
1082
1313
  width: 100%;
1314
+ overflow-wrap: anywhere;
1315
+ }
1316
+ .markdown-section th,
1317
+ .markdown-section td {
1318
+ border-color: rgba(255,255,255,.12);
1083
1319
  }
1084
1320
  .markdown-section blockquote {
1085
- border-left-color: #28d8e8;
1086
- color: #49656f;
1321
+ border-left-color: var(--sap-aqua);
1322
+ color: var(--sap-muted);
1323
+ background: rgba(40,216,232,.045);
1087
1324
  }
1088
1325
  .cover-main,
1089
1326
  section.cover {
1090
- background: linear-gradient(115deg, rgba(40,216,232,.13), transparent 34%), #f8fdff;
1327
+ background:
1328
+ linear-gradient(115deg, rgba(40,216,232,.13), transparent 34%),
1329
+ var(--sap-bg);
1330
+ }
1331
+ @media (max-width: 768px) {
1332
+ .docs-home {
1333
+ position: sticky;
1334
+ top: 8px;
1335
+ left: auto;
1336
+ width: calc(100% - 24px);
1337
+ margin: 8px 12px;
1338
+ justify-content: flex-start;
1339
+ }
1340
+ .app-name {
1341
+ margin-top: 18px;
1342
+ }
1343
+ .markdown-section {
1344
+ padding: 32px 18px 40px;
1345
+ }
1346
+ .markdown-section h1 {
1347
+ font-size: 34px;
1348
+ line-height: 1;
1349
+ }
1350
+ .markdown-section table {
1351
+ display: block;
1352
+ overflow-x: auto;
1353
+ }
1354
+ }
1355
+ @media (max-width: 420px) {
1356
+ .docs-home {
1357
+ border-radius: 20px;
1358
+ }
1359
+ .docs-home span strong {
1360
+ font-size: 14px;
1361
+ }
1091
1362
  }
1092
1363
  </style>
1093
1364
  </head>
1094
1365
  <body>
1366
+ <a class="docs-home" href="${escapeHtml(info.endpoints.landing)}" aria-label="Back to SAP MCP home">
1367
+ <img src="/favicon.png" width="38" height="38" alt="">
1368
+ <span>
1369
+ <strong>SAP MCP Docs</strong>
1370
+ <small>Back to hosted gateway</small>
1371
+ </span>
1372
+ </a>
1095
1373
  <div id="app">Loading SAP MCP documentation...</div>
1096
1374
  <script>
1097
1375
  window.$docsify = ${docsifyConfig};
@@ -1305,7 +1583,11 @@ export class RemoteMCPServer {
1305
1583
  appConfig;
1306
1584
  authManager;
1307
1585
  rateLimiter;
1586
+ requestLimiter;
1308
1587
  statefulSessions = new Map();
1588
+ sessionConfig;
1589
+ httpTuning;
1590
+ sessionCleanupInterval;
1309
1591
  httpServer;
1310
1592
  constructor(config) {
1311
1593
  this.appConfig = loadConfig();
@@ -1317,6 +1599,9 @@ export class RemoteMCPServer {
1317
1599
  this.config = config ?? defaultRemoteConfig(this.appConfig);
1318
1600
  this.authManager = new AuthManager(this.config.auth);
1319
1601
  this.rateLimiter = new RemoteRateLimiter(this.config.rateLimit);
1602
+ this.sessionConfig = this.config.sessions ?? buildRemoteSessionConfigFromEnv();
1603
+ this.httpTuning = this.config.http ?? buildRemoteHttpTuningConfigFromEnv();
1604
+ this.requestLimiter = new InFlightRequestLimiter(this.config.concurrency?.maxInFlightRequests ?? buildRemoteConcurrencyConfigFromEnv().maxInFlightRequests);
1320
1605
  }
1321
1606
  /**
1322
1607
  * @name start
@@ -1325,6 +1610,7 @@ export class RemoteMCPServer {
1325
1610
  async start() {
1326
1611
  this.validateAuthConfig();
1327
1612
  await this.rateLimiter.initialize();
1613
+ this.startSessionCleanup();
1328
1614
  const monetizationGate = await McpMonetizationGate.create(this.appConfig);
1329
1615
  this.httpServer = http.createServer(async (req, res) => {
1330
1616
  if (this.applyCors(req, res)) {
@@ -1345,6 +1631,12 @@ export class RemoteMCPServer {
1345
1631
  }, isHeadMethod(req.method));
1346
1632
  return;
1347
1633
  }
1634
+ if (isPublicReadMethod(req.method) && url.pathname === '/smithery.config.schema.json') {
1635
+ writeJson(res, 200, readSmitheryConfigSchema(), {
1636
+ 'Cache-Control': 'public, max-age=300',
1637
+ }, isHeadMethod(req.method));
1638
+ return;
1639
+ }
1348
1640
  const publicLogoAsset = resolvePublicLogoAsset(req.method, url.pathname);
1349
1641
  if (publicLogoAsset) {
1350
1642
  writeLogoAsset(res, publicLogoAsset);
@@ -1444,6 +1736,21 @@ export class RemoteMCPServer {
1444
1736
  });
1445
1737
  return;
1446
1738
  }
1739
+ const releaseRequest = this.requestLimiter.tryAcquire();
1740
+ if (!releaseRequest) {
1741
+ writeJson(res, 503, {
1742
+ error: 'remote_mcp_over_capacity',
1743
+ retryAfterSeconds: 2,
1744
+ hint: 'The hosted MCP gateway is protecting itself from overload. Retry shortly or reduce parallel calls.',
1745
+ }, {
1746
+ 'Retry-After': '2',
1747
+ 'X-MCP-In-Flight': String(this.requestLimiter.getActiveCount()),
1748
+ });
1749
+ return;
1750
+ }
1751
+ res.once('finish', releaseRequest);
1752
+ res.once('close', releaseRequest);
1753
+ res.setHeader('X-MCP-In-Flight', String(this.requestLimiter.getActiveCount()));
1447
1754
  // ── Request logging ─────────────────────────────────────────────
1448
1755
  const reqStart = Date.now();
1449
1756
  const callerIp = getRateLimitKey(req);
@@ -1505,6 +1812,23 @@ export class RemoteMCPServer {
1505
1812
  }
1506
1813
  }
1507
1814
  catch (error) {
1815
+ if (error instanceof RemoteCapacityError) {
1816
+ logger.warn('Remote MCP capacity limit reached', {
1817
+ error: error.message,
1818
+ activeSessions: this.statefulSessions.size,
1819
+ });
1820
+ if (!res.headersSent) {
1821
+ writeJson(res, 503, {
1822
+ error: 'remote_mcp_session_capacity',
1823
+ retryAfterSeconds: 5,
1824
+ activeSessions: this.statefulSessions.size,
1825
+ maxStatefulSessions: this.sessionConfig.maxStatefulSessions,
1826
+ }, {
1827
+ 'Retry-After': '5',
1828
+ });
1829
+ }
1830
+ return;
1831
+ }
1508
1832
  logger.error('Remote MCP request failed', { error });
1509
1833
  if (!res.headersSent) {
1510
1834
  writeJson(res, 500, {
@@ -1518,6 +1842,10 @@ export class RemoteMCPServer {
1518
1842
  }
1519
1843
  }
1520
1844
  });
1845
+ this.httpServer.requestTimeout = this.httpTuning.requestTimeoutMs;
1846
+ this.httpServer.headersTimeout = this.httpTuning.headersTimeoutMs;
1847
+ this.httpServer.keepAliveTimeout = this.httpTuning.keepAliveTimeoutMs;
1848
+ this.httpServer.maxRequestsPerSocket = this.httpTuning.maxRequestsPerSocket;
1521
1849
  await new Promise((resolve, reject) => {
1522
1850
  this.httpServer?.once('error', reject);
1523
1851
  this.httpServer?.listen(this.config.port, this.config.host, resolve);
@@ -1532,6 +1860,10 @@ export class RemoteMCPServer {
1532
1860
  stateless: this.config.stateless,
1533
1861
  rateLimitPerMinute: this.config.rateLimit.enabled ? this.config.rateLimit.requestsPerMinute : 0,
1534
1862
  redisRateLimit: Boolean(this.config.rateLimit.redisUrl),
1863
+ maxStatefulSessions: this.sessionConfig.maxStatefulSessions,
1864
+ sessionIdleTtlMs: this.sessionConfig.idleTtlMs,
1865
+ maxInFlightRequests: this.config.concurrency?.maxInFlightRequests,
1866
+ requestTimeoutMs: this.httpTuning.requestTimeoutMs,
1535
1867
  });
1536
1868
  }
1537
1869
  /**
@@ -1539,6 +1871,10 @@ export class RemoteMCPServer {
1539
1871
  * @description Gracefully closes the MCP transport and HTTP listener.
1540
1872
  */
1541
1873
  async stop() {
1874
+ if (this.sessionCleanupInterval) {
1875
+ clearInterval(this.sessionCleanupInterval);
1876
+ this.sessionCleanupInterval = undefined;
1877
+ }
1542
1878
  const sessions = Array.from(this.statefulSessions.values());
1543
1879
  this.statefulSessions.clear();
1544
1880
  await Promise.allSettled(sessions.map(session => session.transport.close()));
@@ -1644,6 +1980,10 @@ export class RemoteMCPServer {
1644
1980
  * @description Creates one MCP server and Streamable HTTP transport for a single initialized session.
1645
1981
  */
1646
1982
  async createStatefulSessionTransport() {
1983
+ await this.pruneStatefulSessions('before-create');
1984
+ if (this.statefulSessions.size >= this.sessionConfig.maxStatefulSessions) {
1985
+ throw new RemoteCapacityError(`Stateful MCP session capacity reached (${this.sessionConfig.maxStatefulSessions})`);
1986
+ }
1647
1987
  const server = await createSapMcpServer(this.appConfig);
1648
1988
  const transport = new StreamableHTTPServerTransport({
1649
1989
  sessionIdGenerator: () => randomUUID(),
@@ -1663,16 +2003,85 @@ export class RemoteMCPServer {
1663
2003
  transport.onclose = () => {
1664
2004
  const sessionId = transport.sessionId;
1665
2005
  if (sessionId) {
1666
- this.statefulSessions.delete(sessionId);
1667
- logger.info('Stateful MCP session closed', {
1668
- sessionId,
1669
- activeSessions: this.statefulSessions.size,
1670
- });
2006
+ const deleted = this.statefulSessions.delete(sessionId);
2007
+ if (deleted) {
2008
+ logger.info('Stateful MCP session closed', {
2009
+ sessionId,
2010
+ activeSessions: this.statefulSessions.size,
2011
+ });
2012
+ }
1671
2013
  }
1672
2014
  };
1673
2015
  await server.connect(transport);
1674
2016
  return transport;
1675
2017
  }
2018
+ startSessionCleanup() {
2019
+ if (this.config.stateless || this.sessionCleanupInterval) {
2020
+ return;
2021
+ }
2022
+ this.sessionCleanupInterval = setInterval(() => {
2023
+ void this.pruneStatefulSessions('interval').catch(error => {
2024
+ logger.warn('Stateful MCP session cleanup failed', { error });
2025
+ });
2026
+ }, this.sessionConfig.cleanupIntervalMs);
2027
+ this.sessionCleanupInterval.unref?.();
2028
+ }
2029
+ async pruneStatefulSessions(reason) {
2030
+ if (this.statefulSessions.size === 0) {
2031
+ return;
2032
+ }
2033
+ const now = Date.now();
2034
+ const expiredSessionIds = [];
2035
+ for (const [sessionId, session] of this.statefulSessions.entries()) {
2036
+ const idleMs = now - session.lastSeenAt;
2037
+ const ageMs = now - session.createdAt;
2038
+ if (idleMs > this.sessionConfig.idleTtlMs || ageMs > this.sessionConfig.absoluteTtlMs) {
2039
+ expiredSessionIds.push(sessionId);
2040
+ }
2041
+ }
2042
+ for (const sessionId of expiredSessionIds) {
2043
+ await this.closeStatefulSession(sessionId, 'expired');
2044
+ }
2045
+ if (this.statefulSessions.size < this.sessionConfig.maxStatefulSessions) {
2046
+ if (expiredSessionIds.length > 0) {
2047
+ logger.info('Stateful MCP session cleanup completed', {
2048
+ reason,
2049
+ closed: expiredSessionIds.length,
2050
+ activeSessions: this.statefulSessions.size,
2051
+ });
2052
+ }
2053
+ return;
2054
+ }
2055
+ const targetSize = Math.max(0, this.sessionConfig.maxStatefulSessions - 1);
2056
+ const sessionsByIdleAge = Array.from(this.statefulSessions.entries())
2057
+ .sort(([, left], [, right]) => left.lastSeenAt - right.lastSeenAt);
2058
+ const overflowCount = Math.max(0, this.statefulSessions.size - targetSize);
2059
+ for (const [sessionId] of sessionsByIdleAge.slice(0, overflowCount)) {
2060
+ await this.closeStatefulSession(sessionId, 'capacity-prune');
2061
+ }
2062
+ if (expiredSessionIds.length > 0 || overflowCount > 0) {
2063
+ logger.info('Stateful MCP session cleanup completed', {
2064
+ reason,
2065
+ expired: expiredSessionIds.length,
2066
+ prunedForCapacity: overflowCount,
2067
+ activeSessions: this.statefulSessions.size,
2068
+ });
2069
+ }
2070
+ }
2071
+ async closeStatefulSession(sessionId, reason) {
2072
+ const session = this.statefulSessions.get(sessionId);
2073
+ if (!session) {
2074
+ return;
2075
+ }
2076
+ this.statefulSessions.delete(sessionId);
2077
+ await session.transport.close().catch(error => {
2078
+ logger.warn('Failed to close stateful MCP session', {
2079
+ sessionId,
2080
+ reason,
2081
+ error,
2082
+ });
2083
+ });
2084
+ }
1676
2085
  /**
1677
2086
  * @name applyCors
1678
2087
  * @description Applies configured CORS headers and handles preflight requests.