@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.
- package/.env.example +30 -0
- package/CHANGELOG.md +59 -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/03_PAYMENTS_X402_PAYSH.md +11 -1
- package/USER_DOCS/04_CLIENT_CONFIGS.md +4 -3
- package/USER_DOCS/05_SKILLS_AND_TOOLS.md +27 -3
- package/USER_DOCS/06_DESKTOP_GUI_WIZARD.md +14 -7
- 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/config/mcp-client-injection.js +2 -2
- package/dist/config/mcp-client-injection.js.map +1 -1
- package/dist/config/paths.d.ts +6 -0
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +14 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/wizard.js +3 -3
- package/dist/config/wizard.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 +21 -3
- package/dist/payments/pricing.js.map +1 -1
- package/dist/payments/x402-paid-call.d.ts +96 -0
- package/dist/payments/x402-paid-call.d.ts.map +1 -1
- package/dist/payments/x402-paid-call.js +264 -2
- package/dist/payments/x402-paid-call.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 +8 -1
- 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 +76 -4
- 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/public-home/types.d.ts +2 -0
- package/dist/remote/public-home/types.d.ts.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 +72 -1
- package/dist/remote/server.d.ts.map +1 -1
- package/dist/remote/server.js +424 -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/dist/tools/x402-paid-call-tool.d.ts.map +1 -1
- package/dist/tools/x402-paid-call-tool.js +50 -4
- package/dist/tools/x402-paid-call-tool.js.map +1 -1
- package/dist/tui/config-wizard.js +2 -2
- package/dist/tui/config-wizard.js.map +1 -1
- package/dist/tui/wizard-save.d.ts.map +1 -1
- package/dist/tui/wizard-save.js +7 -3
- package/dist/tui/wizard-save.js.map +1 -1
- package/dist/wizard-core/desktop-flow.d.ts +1 -1
- package/dist/wizard-core/desktop-flow.d.ts.map +1 -1
- package/dist/wizard-core/desktop-flow.js +6 -5
- package/dist/wizard-core/desktop-flow.js.map +1 -1
- package/docs/03_CONFIGURATION_AND_WIZARD.md +1 -0
- package/docs/05_REMOTE_VPS_DEPLOYMENT.md +32 -0
- package/docs/06_PAYMENTS_X402_AND_PAYSH.md +12 -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 +2 -1
- package/docs/README.md +6 -3
- package/docs/_sidebar.md +1 -0
- package/package.json +3 -2
- package/server.json +3 -3
- package/skills/README.md +2 -2
- package/skills/sap-escrow-settlement/SKILL.md +15 -11
- package/skills/sap-mcp/SKILL.md +39 -20
- 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.
|
|
@@ -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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
1298
|
+
color: var(--sap-aqua);
|
|
1070
1299
|
text-underline-offset: 3px;
|
|
1071
1300
|
}
|
|
1072
1301
|
.markdown-section code {
|
|
1073
|
-
color: #
|
|
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
|
|
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:
|
|
1086
|
-
color:
|
|
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:
|
|
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
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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.
|