@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.
- package/.env.example +30 -0
- package/CHANGELOG.md +42 -1
- package/README.md +19 -2
- package/USER_DOCS/00_START_HERE.md +1 -0
- package/USER_DOCS/01_HOSTED_REMOTE_MCP.md +17 -3
- package/USER_DOCS/04_CLIENT_CONFIGS.md +3 -3
- package/USER_DOCS/05_SKILLS_AND_TOOLS.md +27 -3
- package/USER_DOCS/06_DESKTOP_GUI_WIZARD.md +5 -5
- package/USER_DOCS/07_SMITHERY_AND_MARKETPLACES.md +163 -0
- package/assets/logos/claude.png +0 -0
- package/assets/logos/codex.webp +0 -0
- package/assets/logos/hermes.png +0 -0
- package/assets/logos/openclaw.svg +22 -0
- package/config.schema.json +5 -0
- package/dist/config/env.d.ts +4 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +3 -0
- package/dist/config/env.js.map +1 -1
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.js +1 -1
- package/dist/payments/pricing.d.ts +1 -1
- package/dist/payments/pricing.d.ts.map +1 -1
- package/dist/payments/pricing.js +20 -3
- package/dist/payments/pricing.js.map +1 -1
- package/dist/policy/local-policy-engine.js +1 -1
- package/dist/policy/local-policy-engine.js.map +1 -1
- package/dist/prompts/context/sap-agent-context.prompt.js +6 -0
- package/dist/prompts/context/sap-agent-context.prompt.js.map +1 -1
- package/dist/prompts/context/sap-agent-start.prompt.d.ts +14 -0
- package/dist/prompts/context/sap-agent-start.prompt.d.ts.map +1 -0
- package/dist/prompts/context/sap-agent-start.prompt.js +96 -0
- package/dist/prompts/context/sap-agent-start.prompt.js.map +1 -0
- package/dist/prompts/developer/generate-sap-integration.prompt.js +1 -1
- package/dist/prompts/developer/generate-sap-integration.prompt.js.map +1 -1
- package/dist/prompts/register-prompts.d.ts.map +1 -1
- package/dist/prompts/register-prompts.js +3 -1
- package/dist/prompts/register-prompts.js.map +1 -1
- package/dist/remote/public-home/index.d.ts.map +1 -1
- package/dist/remote/public-home/index.js +2 -1
- package/dist/remote/public-home/index.js.map +1 -1
- package/dist/remote/public-home/scripts.d.ts +1 -1
- package/dist/remote/public-home/scripts.d.ts.map +1 -1
- package/dist/remote/public-home/scripts.js +29 -0
- package/dist/remote/public-home/scripts.js.map +1 -1
- package/dist/remote/public-home/sections.d.ts +5 -0
- package/dist/remote/public-home/sections.d.ts.map +1 -1
- package/dist/remote/public-home/sections.js +73 -6
- package/dist/remote/public-home/sections.js.map +1 -1
- package/dist/remote/public-home/styles.d.ts +1 -1
- package/dist/remote/public-home/styles.d.ts.map +1 -1
- package/dist/remote/public-home/styles.js +460 -16
- package/dist/remote/public-home/styles.js.map +1 -1
- package/dist/remote/rate-limiter.d.ts +2 -0
- package/dist/remote/rate-limiter.d.ts.map +1 -1
- package/dist/remote/rate-limiter.js +23 -0
- package/dist/remote/rate-limiter.js.map +1 -1
- package/dist/remote/server.d.ts +70 -1
- package/dist/remote/server.d.ts.map +1 -1
- package/dist/remote/server.js +412 -15
- package/dist/remote/server.js.map +1 -1
- package/dist/runtime/module-resolution.d.ts.map +1 -1
- package/dist/runtime/module-resolution.js +41 -8
- package/dist/runtime/module-resolution.js.map +1 -1
- package/dist/schemas/settlement.schema.d.ts +2 -2
- package/dist/security/tool-permissions.d.ts.map +1 -1
- package/dist/security/tool-permissions.js +1 -6
- package/dist/security/tool-permissions.js.map +1 -1
- package/dist/tools/agent-start-tool.d.ts +12 -0
- package/dist/tools/agent-start-tool.d.ts.map +1 -0
- package/dist/tools/agent-start-tool.js +156 -0
- package/dist/tools/agent-start-tool.js.map +1 -0
- package/dist/tools/client-sdk-tools.d.ts +7 -0
- package/dist/tools/client-sdk-tools.d.ts.map +1 -1
- package/dist/tools/client-sdk-tools.js +80 -4
- package/dist/tools/client-sdk-tools.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/register-tools.d.ts.map +1 -1
- package/dist/tools/register-tools.js +5 -1
- package/dist/tools/register-tools.js.map +1 -1
- package/dist/tools/sap-sdk-tools.d.ts +1 -1
- package/dist/tools/sap-sdk-tools.d.ts.map +1 -1
- package/dist/tools/sap-sdk-tools.js +108 -105
- package/dist/tools/sap-sdk-tools.js.map +1 -1
- package/dist/tools/sap-sns-tools.d.ts +4 -4
- package/dist/tools/sap-sns-tools.js +4 -4
- package/dist/tools/skills-tools.js +1 -1
- package/dist/tools/skills-tools.js.map +1 -1
- package/docs/05_REMOTE_VPS_DEPLOYMENT.md +32 -0
- package/docs/06_PAYMENTS_X402_AND_PAYSH.md +11 -0
- package/docs/07_ENDPOINTS_AND_CLIENTS.md +1 -1
- package/docs/09_TOOLS_SKILLS_AND_AGENT_GUIDE.md +42 -11
- package/docs/14_DESKTOP_WIZARD_RELEASE.md +1 -1
- package/docs/README.md +6 -3
- package/docs/_sidebar.md +1 -0
- package/package.json +3 -2
- package/skills/README.md +2 -2
- package/skills/sap-escrow-settlement/SKILL.md +15 -11
- package/skills/sap-mcp/SKILL.md +26 -11
- package/skills/sap-mcp/TOOL_REFERENCE.md +6 -5
- package/skills/sap-operations/SKILL.md +6 -7
- package/smithery.config.schema.json +59 -0
package/dist/remote/server.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
1298
|
+
color: var(--sap-aqua);
|
|
1082
1299
|
text-underline-offset: 3px;
|
|
1083
1300
|
}
|
|
1084
1301
|
.markdown-section code {
|
|
1085
|
-
color: #
|
|
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
|
|
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:
|
|
1098
|
-
color:
|
|
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:
|
|
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
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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.
|