@oobe-protocol-labs/sap-mcp-server 0.9.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 (104) hide show
  1. package/.env.example +30 -0
  2. package/CHANGELOG.md +42 -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/04_CLIENT_CONFIGS.md +3 -3
  7. package/USER_DOCS/05_SKILLS_AND_TOOLS.md +27 -3
  8. package/USER_DOCS/06_DESKTOP_GUI_WIZARD.md +5 -5
  9. package/USER_DOCS/07_SMITHERY_AND_MARKETPLACES.md +163 -0
  10. package/assets/logos/claude.png +0 -0
  11. package/assets/logos/codex.webp +0 -0
  12. package/assets/logos/hermes.png +0 -0
  13. package/assets/logos/openclaw.svg +22 -0
  14. package/config.schema.json +5 -0
  15. package/dist/config/env.d.ts +4 -0
  16. package/dist/config/env.d.ts.map +1 -1
  17. package/dist/config/env.js +3 -0
  18. package/dist/config/env.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/pricing.d.ts +1 -1
  22. package/dist/payments/pricing.d.ts.map +1 -1
  23. package/dist/payments/pricing.js +20 -3
  24. package/dist/payments/pricing.js.map +1 -1
  25. package/dist/policy/local-policy-engine.js +1 -1
  26. package/dist/policy/local-policy-engine.js.map +1 -1
  27. package/dist/prompts/context/sap-agent-context.prompt.js +6 -0
  28. package/dist/prompts/context/sap-agent-context.prompt.js.map +1 -1
  29. package/dist/prompts/context/sap-agent-start.prompt.d.ts +14 -0
  30. package/dist/prompts/context/sap-agent-start.prompt.d.ts.map +1 -0
  31. package/dist/prompts/context/sap-agent-start.prompt.js +96 -0
  32. package/dist/prompts/context/sap-agent-start.prompt.js.map +1 -0
  33. package/dist/prompts/developer/generate-sap-integration.prompt.js +1 -1
  34. package/dist/prompts/developer/generate-sap-integration.prompt.js.map +1 -1
  35. package/dist/prompts/register-prompts.d.ts.map +1 -1
  36. package/dist/prompts/register-prompts.js +3 -1
  37. package/dist/prompts/register-prompts.js.map +1 -1
  38. package/dist/remote/public-home/index.d.ts.map +1 -1
  39. package/dist/remote/public-home/index.js +2 -1
  40. package/dist/remote/public-home/index.js.map +1 -1
  41. package/dist/remote/public-home/scripts.d.ts +1 -1
  42. package/dist/remote/public-home/scripts.d.ts.map +1 -1
  43. package/dist/remote/public-home/scripts.js +29 -0
  44. package/dist/remote/public-home/scripts.js.map +1 -1
  45. package/dist/remote/public-home/sections.d.ts +5 -0
  46. package/dist/remote/public-home/sections.d.ts.map +1 -1
  47. package/dist/remote/public-home/sections.js +73 -6
  48. package/dist/remote/public-home/sections.js.map +1 -1
  49. package/dist/remote/public-home/styles.d.ts +1 -1
  50. package/dist/remote/public-home/styles.d.ts.map +1 -1
  51. package/dist/remote/public-home/styles.js +460 -16
  52. package/dist/remote/public-home/styles.js.map +1 -1
  53. package/dist/remote/rate-limiter.d.ts +2 -0
  54. package/dist/remote/rate-limiter.d.ts.map +1 -1
  55. package/dist/remote/rate-limiter.js +23 -0
  56. package/dist/remote/rate-limiter.js.map +1 -1
  57. package/dist/remote/server.d.ts +70 -1
  58. package/dist/remote/server.d.ts.map +1 -1
  59. package/dist/remote/server.js +412 -15
  60. package/dist/remote/server.js.map +1 -1
  61. package/dist/runtime/module-resolution.d.ts.map +1 -1
  62. package/dist/runtime/module-resolution.js +41 -8
  63. package/dist/runtime/module-resolution.js.map +1 -1
  64. package/dist/schemas/settlement.schema.d.ts +2 -2
  65. package/dist/security/tool-permissions.d.ts.map +1 -1
  66. package/dist/security/tool-permissions.js +1 -6
  67. package/dist/security/tool-permissions.js.map +1 -1
  68. package/dist/tools/agent-start-tool.d.ts +12 -0
  69. package/dist/tools/agent-start-tool.d.ts.map +1 -0
  70. package/dist/tools/agent-start-tool.js +156 -0
  71. package/dist/tools/agent-start-tool.js.map +1 -0
  72. package/dist/tools/client-sdk-tools.d.ts +7 -0
  73. package/dist/tools/client-sdk-tools.d.ts.map +1 -1
  74. package/dist/tools/client-sdk-tools.js +80 -4
  75. package/dist/tools/client-sdk-tools.js.map +1 -1
  76. package/dist/tools/index.d.ts +1 -0
  77. package/dist/tools/index.d.ts.map +1 -1
  78. package/dist/tools/index.js +3 -1
  79. package/dist/tools/index.js.map +1 -1
  80. package/dist/tools/register-tools.d.ts.map +1 -1
  81. package/dist/tools/register-tools.js +5 -1
  82. package/dist/tools/register-tools.js.map +1 -1
  83. package/dist/tools/sap-sdk-tools.d.ts +1 -1
  84. package/dist/tools/sap-sdk-tools.d.ts.map +1 -1
  85. package/dist/tools/sap-sdk-tools.js +108 -105
  86. package/dist/tools/sap-sdk-tools.js.map +1 -1
  87. package/dist/tools/sap-sns-tools.d.ts +4 -4
  88. package/dist/tools/sap-sns-tools.js +4 -4
  89. package/dist/tools/skills-tools.js +1 -1
  90. package/dist/tools/skills-tools.js.map +1 -1
  91. package/docs/05_REMOTE_VPS_DEPLOYMENT.md +32 -0
  92. package/docs/06_PAYMENTS_X402_AND_PAYSH.md +11 -0
  93. package/docs/07_ENDPOINTS_AND_CLIENTS.md +1 -1
  94. package/docs/09_TOOLS_SKILLS_AND_AGENT_GUIDE.md +42 -11
  95. package/docs/14_DESKTOP_WIZARD_RELEASE.md +1 -1
  96. package/docs/README.md +6 -3
  97. package/docs/_sidebar.md +1 -0
  98. package/package.json +3 -2
  99. package/skills/README.md +2 -2
  100. package/skills/sap-escrow-settlement/SKILL.md +15 -11
  101. package/skills/sap-mcp/SKILL.md +26 -11
  102. package/skills/sap-mcp/TOOL_REFERENCE.md +6 -5
  103. package/skills/sap-operations/SKILL.md +6 -7
  104. 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.
@@ -619,6 +688,7 @@ export function buildPublicServerInfo(req, config) {
619
688
  serverInfo: `${baseUrl}/server.json`,
620
689
  openApi: `${baseUrl}/openapi.json`,
621
690
  x402Discovery: `${baseUrl}/.well-known/x402`,
691
+ smitheryConfigSchema: `${baseUrl}/smithery.config.schema.json`,
622
692
  smitheryServerCard: `${baseUrl}/.well-known/mcp/server-card.json`,
623
693
  payShProvider: `${baseUrl}/pay/provider.yml`,
624
694
  agentCard: `${baseUrl}/.well-known/agent-card.json`,
@@ -662,6 +732,34 @@ export function buildPublicServerInfo(req, config) {
662
732
  },
663
733
  };
664
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
+ }
665
763
  /**
666
764
  * @name getStaticServerCardCapabilities
667
765
  * @description Reads capabilities from the real MCP registration store so static metadata never drifts from tools/list.
@@ -739,6 +837,7 @@ export async function buildStaticServerCard(req, config, appConfig) {
739
837
  required: authRequired,
740
838
  schemes: authRequired ? ['Bearer'] : ['none', 'x402'],
741
839
  },
840
+ configuration: buildMarketplaceConfigurationMetadata(req, config),
742
841
  transport: {
743
842
  type: 'streamable-http',
744
843
  url: `${baseUrl}/mcp`,
@@ -1058,52 +1157,219 @@ export function buildDocsHtml(req, config) {
1058
1157
  :root {
1059
1158
  --theme-color: #28d8e8;
1060
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);
1061
1172
  }
1062
1173
  body {
1063
1174
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1064
- 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);
1065
1251
  }
1066
1252
  .app-name-link {
1067
- color: #062730;
1253
+ color: var(--sap-ink);
1068
1254
  font-weight: 800;
1069
1255
  }
1256
+ .app-name {
1257
+ margin-top: 82px;
1258
+ }
1070
1259
  .sidebar {
1071
- 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;
1072
1271
  }
1073
1272
  .sidebar-nav strong,
1074
1273
  .markdown-section h1,
1075
1274
  .markdown-section h2,
1076
1275
  .markdown-section h3 {
1077
- color: #062730;
1276
+ color: var(--sap-ink);
1078
1277
  letter-spacing: 0;
1079
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
+ }
1080
1297
  .markdown-section a {
1081
- color: #087f91;
1298
+ color: var(--sap-aqua);
1082
1299
  text-underline-offset: 3px;
1083
1300
  }
1084
1301
  .markdown-section code {
1085
- color: #062730;
1302
+ color: #d8faff;
1086
1303
  border-radius: 6px;
1304
+ background: rgba(255,255,255,.08);
1087
1305
  }
1088
1306
  .markdown-section pre {
1089
1307
  border-radius: 8px;
1090
- border: 1px solid rgba(6,39,48,.10);
1308
+ border: 1px solid var(--sap-line);
1309
+ background: rgba(0,0,0,.34);
1091
1310
  }
1092
1311
  .markdown-section table {
1093
1312
  display: table;
1094
1313
  width: 100%;
1314
+ overflow-wrap: anywhere;
1315
+ }
1316
+ .markdown-section th,
1317
+ .markdown-section td {
1318
+ border-color: rgba(255,255,255,.12);
1095
1319
  }
1096
1320
  .markdown-section blockquote {
1097
- border-left-color: #28d8e8;
1098
- color: #49656f;
1321
+ border-left-color: var(--sap-aqua);
1322
+ color: var(--sap-muted);
1323
+ background: rgba(40,216,232,.045);
1099
1324
  }
1100
1325
  .cover-main,
1101
1326
  section.cover {
1102
- 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
+ }
1103
1362
  }
1104
1363
  </style>
1105
1364
  </head>
1106
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>
1107
1373
  <div id="app">Loading SAP MCP documentation...</div>
1108
1374
  <script>
1109
1375
  window.$docsify = ${docsifyConfig};
@@ -1317,7 +1583,11 @@ export class RemoteMCPServer {
1317
1583
  appConfig;
1318
1584
  authManager;
1319
1585
  rateLimiter;
1586
+ requestLimiter;
1320
1587
  statefulSessions = new Map();
1588
+ sessionConfig;
1589
+ httpTuning;
1590
+ sessionCleanupInterval;
1321
1591
  httpServer;
1322
1592
  constructor(config) {
1323
1593
  this.appConfig = loadConfig();
@@ -1329,6 +1599,9 @@ export class RemoteMCPServer {
1329
1599
  this.config = config ?? defaultRemoteConfig(this.appConfig);
1330
1600
  this.authManager = new AuthManager(this.config.auth);
1331
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);
1332
1605
  }
1333
1606
  /**
1334
1607
  * @name start
@@ -1337,6 +1610,7 @@ export class RemoteMCPServer {
1337
1610
  async start() {
1338
1611
  this.validateAuthConfig();
1339
1612
  await this.rateLimiter.initialize();
1613
+ this.startSessionCleanup();
1340
1614
  const monetizationGate = await McpMonetizationGate.create(this.appConfig);
1341
1615
  this.httpServer = http.createServer(async (req, res) => {
1342
1616
  if (this.applyCors(req, res)) {
@@ -1357,6 +1631,12 @@ export class RemoteMCPServer {
1357
1631
  }, isHeadMethod(req.method));
1358
1632
  return;
1359
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
+ }
1360
1640
  const publicLogoAsset = resolvePublicLogoAsset(req.method, url.pathname);
1361
1641
  if (publicLogoAsset) {
1362
1642
  writeLogoAsset(res, publicLogoAsset);
@@ -1456,6 +1736,21 @@ export class RemoteMCPServer {
1456
1736
  });
1457
1737
  return;
1458
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()));
1459
1754
  // ── Request logging ─────────────────────────────────────────────
1460
1755
  const reqStart = Date.now();
1461
1756
  const callerIp = getRateLimitKey(req);
@@ -1517,6 +1812,23 @@ export class RemoteMCPServer {
1517
1812
  }
1518
1813
  }
1519
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
+ }
1520
1832
  logger.error('Remote MCP request failed', { error });
1521
1833
  if (!res.headersSent) {
1522
1834
  writeJson(res, 500, {
@@ -1530,6 +1842,10 @@ export class RemoteMCPServer {
1530
1842
  }
1531
1843
  }
1532
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;
1533
1849
  await new Promise((resolve, reject) => {
1534
1850
  this.httpServer?.once('error', reject);
1535
1851
  this.httpServer?.listen(this.config.port, this.config.host, resolve);
@@ -1544,6 +1860,10 @@ export class RemoteMCPServer {
1544
1860
  stateless: this.config.stateless,
1545
1861
  rateLimitPerMinute: this.config.rateLimit.enabled ? this.config.rateLimit.requestsPerMinute : 0,
1546
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,
1547
1867
  });
1548
1868
  }
1549
1869
  /**
@@ -1551,6 +1871,10 @@ export class RemoteMCPServer {
1551
1871
  * @description Gracefully closes the MCP transport and HTTP listener.
1552
1872
  */
1553
1873
  async stop() {
1874
+ if (this.sessionCleanupInterval) {
1875
+ clearInterval(this.sessionCleanupInterval);
1876
+ this.sessionCleanupInterval = undefined;
1877
+ }
1554
1878
  const sessions = Array.from(this.statefulSessions.values());
1555
1879
  this.statefulSessions.clear();
1556
1880
  await Promise.allSettled(sessions.map(session => session.transport.close()));
@@ -1656,6 +1980,10 @@ export class RemoteMCPServer {
1656
1980
  * @description Creates one MCP server and Streamable HTTP transport for a single initialized session.
1657
1981
  */
1658
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
+ }
1659
1987
  const server = await createSapMcpServer(this.appConfig);
1660
1988
  const transport = new StreamableHTTPServerTransport({
1661
1989
  sessionIdGenerator: () => randomUUID(),
@@ -1675,16 +2003,85 @@ export class RemoteMCPServer {
1675
2003
  transport.onclose = () => {
1676
2004
  const sessionId = transport.sessionId;
1677
2005
  if (sessionId) {
1678
- this.statefulSessions.delete(sessionId);
1679
- logger.info('Stateful MCP session closed', {
1680
- sessionId,
1681
- activeSessions: this.statefulSessions.size,
1682
- });
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
+ }
1683
2013
  }
1684
2014
  };
1685
2015
  await server.connect(transport);
1686
2016
  return transport;
1687
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
+ }
1688
2085
  /**
1689
2086
  * @name applyCors
1690
2087
  * @description Applies configured CORS headers and handles preflight requests.