@oobe-protocol-labs/sap-mcp-server 0.3.0 → 0.7.0
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 +3 -0
- package/CHANGELOG.md +218 -2
- package/README.md +63 -0
- package/USER_DOCS/00_START_HERE.md +23 -1
- package/USER_DOCS/01_HOSTED_REMOTE_MCP.md +4 -1
- package/USER_DOCS/03_PAYMENTS_X402_PAYSH.md +63 -7
- package/USER_DOCS/04_CLIENT_CONFIGS.md +134 -6
- package/USER_DOCS/06_DESKTOP_GUI_WIZARD.md +262 -0
- package/dist/adapters/mcp/sdk-compat.d.ts.map +1 -1
- package/dist/adapters/mcp/sdk-compat.js +32 -18
- package/dist/adapters/mcp/sdk-compat.js.map +1 -1
- package/dist/config/mcp-client-injection.d.ts +64 -2
- package/dist/config/mcp-client-injection.d.ts.map +1 -1
- package/dist/config/mcp-client-injection.js +668 -11
- package/dist/config/mcp-client-injection.js.map +1 -1
- package/dist/config/wizard.d.ts.map +1 -1
- package/dist/config/wizard.js +89 -5
- 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/facilitator-rpc-fallback.d.ts +43 -0
- package/dist/payments/facilitator-rpc-fallback.d.ts.map +1 -0
- package/dist/payments/facilitator-rpc-fallback.js +228 -0
- package/dist/payments/facilitator-rpc-fallback.js.map +1 -0
- package/dist/payments/json-rpc.d.ts.map +1 -1
- package/dist/payments/json-rpc.js +2 -1
- package/dist/payments/json-rpc.js.map +1 -1
- package/dist/payments/monetization-gate.d.ts +43 -2
- package/dist/payments/monetization-gate.d.ts.map +1 -1
- package/dist/payments/monetization-gate.js +618 -9
- package/dist/payments/monetization-gate.js.map +1 -1
- package/dist/payments/oobe-facilitator-server.d.ts +6 -0
- package/dist/payments/oobe-facilitator-server.d.ts.map +1 -1
- package/dist/payments/oobe-facilitator-server.js +79 -4
- package/dist/payments/oobe-facilitator-server.js.map +1 -1
- package/dist/payments/pricing.d.ts.map +1 -1
- package/dist/payments/pricing.js +1 -0
- package/dist/payments/pricing.js.map +1 -1
- package/dist/payments/usage-ledger.d.ts +10 -0
- package/dist/payments/usage-ledger.d.ts.map +1 -1
- package/dist/payments/usage-ledger.js +51 -0
- package/dist/payments/usage-ledger.js.map +1 -1
- package/dist/payments/x402-paid-call.d.ts +53 -0
- package/dist/payments/x402-paid-call.d.ts.map +1 -0
- package/dist/payments/x402-paid-call.js +370 -0
- package/dist/payments/x402-paid-call.js.map +1 -0
- package/dist/prompts/context/sap-agent-context.prompt.js +9 -3
- package/dist/prompts/context/sap-agent-context.prompt.js.map +1 -1
- package/dist/prompts/developer/debug-sap-error.prompt.js +1 -1
- package/dist/prompts/developer/debug-sap-error.prompt.js.map +1 -1
- package/dist/prompts/developer/generate-sap-integration.prompt.js +6 -2
- package/dist/prompts/developer/generate-sap-integration.prompt.js.map +1 -1
- package/dist/prompts/execution-proof/explain-proof-of-execution.prompt.js +1 -1
- package/dist/prompts/execution-proof/explain-proof-of-execution.prompt.js.map +1 -1
- package/dist/prompts/execution-proof/verify-execution-proof.prompt.js +1 -1
- package/dist/prompts/execution-proof/verify-execution-proof.prompt.js.map +1 -1
- package/dist/prompts/payments/create-paid-api.prompt.js +1 -1
- package/dist/prompts/payments/create-paid-api.prompt.js.map +1 -1
- package/dist/prompts/payments/explain-x402-settlement.prompt.js +1 -1
- package/dist/prompts/payments/explain-x402-settlement.prompt.js.map +1 -1
- package/dist/prompts/register-prompts.js +2 -2
- package/dist/prompts/register-prompts.js.map +1 -1
- package/dist/prompts/registry/analyze-sap-agent.prompt.js +1 -1
- package/dist/prompts/registry/analyze-sap-agent.prompt.js.map +1 -1
- package/dist/prompts/registry/register-sap-agent.prompt.js +2 -2
- package/dist/prompts/registry/register-sap-agent.prompt.js.map +1 -1
- package/dist/remote/server.d.ts +132 -1
- package/dist/remote/server.d.ts.map +1 -1
- package/dist/remote/server.js +885 -49
- package/dist/remote/server.js.map +1 -1
- package/dist/resources/current/sap-current-config.resource.d.ts.map +1 -1
- package/dist/resources/current/sap-current-config.resource.js +11 -1
- package/dist/resources/current/sap-current-config.resource.js.map +1 -1
- package/dist/resources/execution-proof/sap-execution-record.resource.js +1 -1
- package/dist/resources/execution-proof/sap-execution-record.resource.js.map +1 -1
- package/dist/resources/memory/sap-memory.resource.js +1 -1
- package/dist/resources/memory/sap-memory.resource.js.map +1 -1
- package/dist/resources/profile/sap-active-profile.resource.js +1 -1
- package/dist/resources/profile/sap-active-profile.resource.js.map +1 -1
- package/dist/resources/register-resources.js +2 -2
- package/dist/resources/register-resources.js.map +1 -1
- package/dist/resources/registry/sap-agent.resource.js +1 -1
- package/dist/resources/registry/sap-agent.resource.js.map +1 -1
- package/dist/resources/registry/sap-global-registry.resource.js +1 -1
- package/dist/resources/registry/sap-global-registry.resource.js.map +1 -1
- package/dist/resources/reputation/sap-reputation.resource.js +1 -1
- package/dist/resources/reputation/sap-reputation.resource.js.map +1 -1
- package/dist/resources/stats/sap-network-stats.resource.js +1 -1
- package/dist/resources/stats/sap-network-stats.resource.js.map +1 -1
- package/dist/resources/tool-schema/sap-tool-schema.resource.js +1 -1
- package/dist/resources/tool-schema/sap-tool-schema.resource.js.map +1 -1
- package/dist/sap/sap-client-manager.js +5 -5
- package/dist/sap/sap-client-manager.js.map +1 -1
- package/dist/security/tool-permissions.d.ts.map +1 -1
- package/dist/security/tool-permissions.js +1 -0
- package/dist/security/tool-permissions.js.map +1 -1
- package/dist/server/create-server.js +5 -5
- package/dist/server/create-server.js.map +1 -1
- package/dist/server/register-capabilities.js +4 -4
- package/dist/server/register-capabilities.js.map +1 -1
- package/dist/signer/signer-resolver.js +1 -1
- package/dist/signer/signer-resolver.js.map +1 -1
- package/dist/tools/client-sdk-tools.js +7 -7
- package/dist/tools/client-sdk-tools.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/magicblock-tools.d.ts +28 -0
- package/dist/tools/magicblock-tools.d.ts.map +1 -0
- package/dist/tools/magicblock-tools.js +578 -0
- package/dist/tools/magicblock-tools.js.map +1 -0
- package/dist/tools/profile-tools.d.ts.map +1 -1
- package/dist/tools/profile-tools.js +20 -0
- package/dist/tools/profile-tools.js.map +1 -1
- package/dist/tools/register-tools.d.ts.map +1 -1
- package/dist/tools/register-tools.js +11 -2
- package/dist/tools/register-tools.js.map +1 -1
- package/dist/tools/sap-sdk-tools.js +2 -2
- package/dist/tools/sap-sdk-tools.js.map +1 -1
- package/dist/tools/sap-sns-tools.js +2 -2
- package/dist/tools/sap-sns-tools.js.map +1 -1
- package/dist/tools/skills-tools.d.ts +1 -1
- package/dist/tools/skills-tools.d.ts.map +1 -1
- package/dist/tools/skills-tools.js +10 -1
- package/dist/tools/skills-tools.js.map +1 -1
- package/dist/tools/tool-aliases.d.ts +10 -0
- package/dist/tools/tool-aliases.d.ts.map +1 -0
- package/dist/tools/tool-aliases.js +23 -0
- package/dist/tools/tool-aliases.js.map +1 -0
- package/dist/tools/x402-paid-call-tool.d.ts +12 -0
- package/dist/tools/x402-paid-call-tool.d.ts.map +1 -0
- package/dist/tools/x402-paid-call-tool.js +118 -0
- package/dist/tools/x402-paid-call-tool.js.map +1 -0
- package/dist/wizard-core/desktop-flow.d.ts +82 -0
- package/dist/wizard-core/desktop-flow.d.ts.map +1 -0
- package/dist/wizard-core/desktop-flow.js +181 -0
- package/dist/wizard-core/desktop-flow.js.map +1 -0
- package/docs/00_README.md +3 -0
- package/docs/03_CONFIGURATION_AND_WIZARD.md +31 -5
- package/docs/06_PAYMENTS_X402_AND_PAYSH.md +70 -7
- package/docs/07_ENDPOINTS_AND_CLIENTS.md +28 -1
- package/docs/10_OPERATIONS_RELEASE_AND_PM2.md +7 -6
- package/docs/13_BOUNTY_PROGRAM_PROPOSAL.md +469 -0
- package/docs/14_DESKTOP_WIZARD_RELEASE.md +264 -0
- package/docs/README.md +104 -0
- package/docs/_sidebar.md +31 -0
- package/docs/magicblock-tools.md +166 -0
- package/docs/x402-protocol-spec.md +984 -0
- package/package.json +23 -2
- package/server.json +71 -0
- package/skills/sap-defi/SKILL.md +8 -0
- package/skills/sap-mcp/SKILL.md +30 -0
- package/skills/sap-payments-x402/SKILL.md +36 -4
package/dist/remote/server.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import { existsSync, readFileSync } from 'fs';
|
|
8
8
|
import * as http from 'http';
|
|
9
|
-
import { dirname, join } from 'path';
|
|
9
|
+
import { dirname, join, posix } from 'path';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
12
12
|
import { getDataDir, loadConfig } from '../config/env.js';
|
|
@@ -14,12 +14,22 @@ import { MCP_SERVER_VERSION } from '../core/constants.js';
|
|
|
14
14
|
import { logger, initLogger } from '../core/logger.js';
|
|
15
15
|
import { createSapMcpServer } from '../server/create-server.js';
|
|
16
16
|
import { AuthManager } from './auth/index.js';
|
|
17
|
-
import { McpMonetizationGate } from '../payments/index.js';
|
|
17
|
+
import { McpMonetizationGate, resolvePaymentNetwork } from '../payments/index.js';
|
|
18
|
+
import { generatePayShProviderYaml } from '../payments/pay-sh-spec.js';
|
|
18
19
|
import { RemoteRateLimiter, buildRemoteRateLimitConfigFromEnv } from './rate-limiter.js';
|
|
19
20
|
const PUBLIC_SERVER_TITLE = 'SAP MCP Server | OOBE Protocol';
|
|
20
21
|
const PUBLIC_SERVER_DESCRIPTION = 'Hosted Solana-native MCP gateway for Synapse Agent Protocol tools, x402/pay.sh monetization, SNS identity, and agent operations.';
|
|
21
22
|
const LOGO_ASSET_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'assets', 'explorer_logo.png');
|
|
23
|
+
const DOCS_ROOT_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'docs');
|
|
24
|
+
const USER_DOCS_ROOT_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'USER_DOCS');
|
|
22
25
|
const PAYMENT_STATS_CACHE_MS = 15_000;
|
|
26
|
+
const RELEASE_DOWNLOAD_BASE_URL = `https://github.com/OOBE-PROTOCOL/sap-mcp/releases/download/${MCP_SERVER_VERSION}`;
|
|
27
|
+
const WIZARD_NPM_COMMAND = 'npm exec --yes --package @oobe-protocol-labs/sap-mcp-server -- sap-mcp-config wizard';
|
|
28
|
+
const X402_PAID_CALL_NPX_COMMAND = 'npx --yes --package @oobe-protocol-labs/sap-mcp-server sap-mcp-x402-paid-call --tool sap_list_all_agents --arguments \'{"limit":5}\' --max-usd 0.02 --confirm';
|
|
29
|
+
const X402_PAID_CALL_ADDON_PATH = '~/.config/mcp-sap/addons/x402-paid-call';
|
|
30
|
+
const SOLANA_DEVNET_CAIP2 = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1';
|
|
31
|
+
const USDC_MAINNET_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
|
32
|
+
const USDC_DEVNET_MINT = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU';
|
|
23
33
|
let logoAssetCache;
|
|
24
34
|
let paymentStatsCache;
|
|
25
35
|
/**
|
|
@@ -53,8 +63,13 @@ export function defaultRemoteConfig(appConfig) {
|
|
|
53
63
|
port,
|
|
54
64
|
host,
|
|
55
65
|
corsOrigins: appConfig.httpCorsOrigins,
|
|
56
|
-
stateless: parseRemoteBoolean(process.env.SAP_MCP_HTTP_STATELESS,
|
|
66
|
+
stateless: parseRemoteBoolean(process.env.SAP_MCP_HTTP_STATELESS, false),
|
|
57
67
|
rateLimit: buildRemoteRateLimitConfigFromEnv(appConfig.rateLimitPerMinute),
|
|
68
|
+
paymentDiscovery: buildPublicPaymentDiscovery(appConfig),
|
|
69
|
+
paymentPriceRange: {
|
|
70
|
+
minUsd: appConfig.monetization.prices.minUsd,
|
|
71
|
+
maxUsd: appConfig.monetization.prices.maxUsd,
|
|
72
|
+
},
|
|
58
73
|
};
|
|
59
74
|
if (authType === 'none') {
|
|
60
75
|
return {
|
|
@@ -86,6 +101,56 @@ export function defaultRemoteConfig(appConfig) {
|
|
|
86
101
|
},
|
|
87
102
|
};
|
|
88
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* @name buildPublicPaymentDiscovery
|
|
106
|
+
* @description Builds public x402 seller/facilitator coordinates without exposing facilitator URL or auth tokens.
|
|
107
|
+
*/
|
|
108
|
+
function buildPublicPaymentDiscovery(appConfig) {
|
|
109
|
+
if (!appConfig.monetization.enabled || !appConfig.monetization.payTo) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const network = resolvePaymentNetwork(appConfig);
|
|
113
|
+
const facilitatorPublicKey = (process.env.SAP_MCP_X402_FACILITATOR_PUBLIC_KEY
|
|
114
|
+
?? process.env.SAP_MCP_X402_FACILITATOR_FEE_PAYER
|
|
115
|
+
?? '').trim();
|
|
116
|
+
return {
|
|
117
|
+
provider: appConfig.monetization.provider,
|
|
118
|
+
network,
|
|
119
|
+
asset: resolvePublicPaymentAsset(network),
|
|
120
|
+
assetSymbol: 'USDC',
|
|
121
|
+
payTo: appConfig.monetization.payTo,
|
|
122
|
+
seller: {
|
|
123
|
+
name: 'OOBE Protocol',
|
|
124
|
+
payTo: appConfig.monetization.payTo,
|
|
125
|
+
},
|
|
126
|
+
...(facilitatorPublicKey
|
|
127
|
+
? {
|
|
128
|
+
facilitator: {
|
|
129
|
+
role: 'x402-svm-fee-payer',
|
|
130
|
+
publicKey: facilitatorPublicKey,
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
134
|
+
headers: {
|
|
135
|
+
paymentRequired: 'PAYMENT-REQUIRED',
|
|
136
|
+
paymentSignature: 'PAYMENT-SIGNATURE',
|
|
137
|
+
paymentResponse: 'PAYMENT-RESPONSE',
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* @name resolvePublicPaymentAsset
|
|
143
|
+
* @description Resolves the public USDC mint advertised in x402 discovery metadata.
|
|
144
|
+
*/
|
|
145
|
+
function resolvePublicPaymentAsset(network) {
|
|
146
|
+
if (network === SOLANA_DEVNET_CAIP2) {
|
|
147
|
+
return USDC_DEVNET_MINT;
|
|
148
|
+
}
|
|
149
|
+
return USDC_MAINNET_MINT;
|
|
150
|
+
}
|
|
151
|
+
function formatOpenApiUsd(value) {
|
|
152
|
+
return value.toFixed(6).replace(/0+$/, '').replace(/\.$/, '');
|
|
153
|
+
}
|
|
89
154
|
function parseRemoteBoolean(raw, fallback) {
|
|
90
155
|
if (raw === undefined) {
|
|
91
156
|
return fallback;
|
|
@@ -96,38 +161,208 @@ function parseRemoteBoolean(raw, fallback) {
|
|
|
96
161
|
* @name writeJson
|
|
97
162
|
* @description Writes a JSON response with optional additional headers.
|
|
98
163
|
*/
|
|
99
|
-
function writeJson(res, status, body, headers = {}) {
|
|
164
|
+
function writeJson(res, status, body, headers = {}, omitBody = false) {
|
|
165
|
+
const serialized = JSON.stringify(body);
|
|
100
166
|
res.writeHead(status, {
|
|
101
167
|
'Content-Type': 'application/json',
|
|
168
|
+
'Content-Length': Buffer.byteLength(serialized),
|
|
102
169
|
...headers,
|
|
103
170
|
});
|
|
104
|
-
res.end(
|
|
171
|
+
res.end(omitBody ? undefined : serialized);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* @name writeText
|
|
175
|
+
* @description Writes a cacheable text response with an explicit content type.
|
|
176
|
+
*/
|
|
177
|
+
function writeText(res, status, body, contentType, headers = {}, omitBody = false) {
|
|
178
|
+
res.writeHead(status, {
|
|
179
|
+
'Content-Type': contentType,
|
|
180
|
+
'Cache-Control': 'public, max-age=300',
|
|
181
|
+
'Content-Length': Buffer.byteLength(body),
|
|
182
|
+
'X-Content-Type-Options': 'nosniff',
|
|
183
|
+
...headers,
|
|
184
|
+
});
|
|
185
|
+
res.end(omitBody ? undefined : body);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* @name writeMcpJsonRpcError
|
|
189
|
+
* @description Writes an MCP-compatible JSON-RPC error response with HTTP status semantics.
|
|
190
|
+
*/
|
|
191
|
+
function writeMcpJsonRpcError(res, status, code, id, message, data) {
|
|
192
|
+
writeJson(res, status, {
|
|
193
|
+
jsonrpc: '2.0',
|
|
194
|
+
id,
|
|
195
|
+
error: {
|
|
196
|
+
code,
|
|
197
|
+
message,
|
|
198
|
+
...(data === undefined ? {} : { data }),
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* @name readSingleHeader
|
|
204
|
+
* @description Reads one normalized HTTP header value without accepting ambiguous arrays.
|
|
205
|
+
*/
|
|
206
|
+
function readSingleHeader(value) {
|
|
207
|
+
if (Array.isArray(value)) {
|
|
208
|
+
return value[0]?.trim() || undefined;
|
|
209
|
+
}
|
|
210
|
+
return value?.trim() || undefined;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* @name extractJsonRpcId
|
|
214
|
+
* @description Extracts a JSON-RPC id for error correlation.
|
|
215
|
+
*/
|
|
216
|
+
function extractJsonRpcId(body) {
|
|
217
|
+
if (Array.isArray(body) && body.length > 0) {
|
|
218
|
+
return extractJsonRpcId(body[0]);
|
|
219
|
+
}
|
|
220
|
+
if (!body || typeof body !== 'object') {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const record = body;
|
|
224
|
+
return typeof record.id === 'string' || typeof record.id === 'number' ? record.id : null;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* @name isInitializeJsonRpcRequest
|
|
228
|
+
* @description Detects MCP initialize requests that are allowed to create a new stateful session.
|
|
229
|
+
*/
|
|
230
|
+
function isInitializeJsonRpcRequest(body) {
|
|
231
|
+
if (Array.isArray(body)) {
|
|
232
|
+
return body.some(isInitializeJsonRpcRequest);
|
|
233
|
+
}
|
|
234
|
+
if (!body || typeof body !== 'object') {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
return body.method === 'initialize';
|
|
105
238
|
}
|
|
106
239
|
/**
|
|
107
240
|
* @name writeHtml
|
|
108
241
|
* @description Writes a cacheable HTML response for public endpoint previews and social sharing.
|
|
109
242
|
*/
|
|
110
|
-
function writeHtml(res, status, html) {
|
|
243
|
+
function writeHtml(res, status, html, omitBody = false) {
|
|
111
244
|
res.writeHead(status, {
|
|
112
245
|
'Content-Type': 'text/html; charset=utf-8',
|
|
113
246
|
'Cache-Control': 'public, max-age=300',
|
|
247
|
+
'Content-Length': Buffer.byteLength(html),
|
|
248
|
+
'X-Content-Type-Options': 'nosniff',
|
|
249
|
+
});
|
|
250
|
+
res.end(omitBody ? undefined : html);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* @name isPublicReadMethod
|
|
254
|
+
* @description Returns true for cacheable public metadata reads, including crawler-friendly HEAD checks.
|
|
255
|
+
*/
|
|
256
|
+
function isPublicReadMethod(method) {
|
|
257
|
+
return method === 'GET' || method === 'HEAD';
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* @name isHeadMethod
|
|
261
|
+
* @description Returns true when the request must emit headers without a response body.
|
|
262
|
+
*/
|
|
263
|
+
function isHeadMethod(method) {
|
|
264
|
+
return method === 'HEAD';
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* @name writeMarkdownAsset
|
|
268
|
+
* @description Writes a resolved public markdown documentation asset.
|
|
269
|
+
*/
|
|
270
|
+
function writeMarkdownAsset(res, asset) {
|
|
271
|
+
res.writeHead(200, {
|
|
272
|
+
'Content-Type': asset.contentType,
|
|
273
|
+
'Cache-Control': 'public, max-age=120',
|
|
274
|
+
'Content-Length': asset.contentLength,
|
|
114
275
|
'X-Content-Type-Options': 'nosniff',
|
|
115
276
|
});
|
|
116
|
-
res.end(
|
|
277
|
+
res.end(asset.body);
|
|
117
278
|
}
|
|
118
279
|
/**
|
|
119
|
-
* @name
|
|
120
|
-
* @description
|
|
280
|
+
* @name resolvePublicDocsMarkdown
|
|
281
|
+
* @description Resolves safe hosted documentation markdown requests under /docs and /docs/user.
|
|
121
282
|
*/
|
|
122
|
-
function
|
|
283
|
+
export function resolvePublicDocsMarkdown(method, pathname) {
|
|
284
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
if (!pathname.startsWith('/docs/') || !pathname.endsWith('.md')) {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
let decodedPath;
|
|
291
|
+
try {
|
|
292
|
+
decodedPath = decodeURIComponent(pathname.slice('/docs/'.length));
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
const normalizedPath = posix.normalize(decodedPath.replace(/\\/g, '/')).replace(/^[/\\]+/, '');
|
|
298
|
+
if (isUnsafeDocsPath(normalizedPath)) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
const root = normalizedPath.startsWith('user/')
|
|
302
|
+
? USER_DOCS_ROOT_PATH
|
|
303
|
+
: DOCS_ROOT_PATH;
|
|
304
|
+
const relativePath = normalizedPath.startsWith('user/')
|
|
305
|
+
? normalizedPath.slice('user/'.length)
|
|
306
|
+
: normalizedPath;
|
|
307
|
+
if (isUnsafeDocsPath(relativePath)) {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
const absolutePath = join(root, relativePath);
|
|
311
|
+
if (!existsSync(absolutePath)) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
const markdown = readFileSync(absolutePath, 'utf-8');
|
|
315
|
+
return {
|
|
316
|
+
contentType: 'text/markdown; charset=utf-8',
|
|
317
|
+
contentLength: Buffer.byteLength(markdown),
|
|
318
|
+
body: method === 'HEAD' ? undefined : markdown,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function isUnsafeDocsPath(pathname) {
|
|
322
|
+
return pathname === ''
|
|
323
|
+
|| pathname.includes('\0')
|
|
324
|
+
|| pathname === '..'
|
|
325
|
+
|| pathname.startsWith('../')
|
|
326
|
+
|| pathname.includes('/../');
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* @name resolvePublicLogoAsset
|
|
330
|
+
* @description Resolves public icon/social image routes, including HEAD-safe favicon responses for crawlers.
|
|
331
|
+
*/
|
|
332
|
+
export function resolvePublicLogoAsset(method, pathname) {
|
|
333
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
123
336
|
const image = readLogoAsset();
|
|
337
|
+
const body = method === 'HEAD' ? undefined : image;
|
|
338
|
+
if (['/favicon.png', '/apple-touch-icon.png', '/og.png'].includes(pathname)) {
|
|
339
|
+
return {
|
|
340
|
+
contentType: 'image/png',
|
|
341
|
+
contentLength: image.byteLength,
|
|
342
|
+
body,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (pathname === '/favicon.ico') {
|
|
346
|
+
return {
|
|
347
|
+
contentType: 'image/x-icon',
|
|
348
|
+
contentLength: image.byteLength,
|
|
349
|
+
body,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* @name writeLogoAsset
|
|
356
|
+
* @description Writes a resolved public logo asset response.
|
|
357
|
+
*/
|
|
358
|
+
function writeLogoAsset(res, asset) {
|
|
124
359
|
res.writeHead(200, {
|
|
125
|
-
'Content-Type':
|
|
360
|
+
'Content-Type': asset.contentType,
|
|
126
361
|
'Cache-Control': 'public, max-age=86400',
|
|
127
|
-
'Content-Length':
|
|
362
|
+
'Content-Length': asset.contentLength,
|
|
128
363
|
'X-Content-Type-Options': 'nosniff',
|
|
129
364
|
});
|
|
130
|
-
res.end(
|
|
365
|
+
res.end(asset.body);
|
|
131
366
|
}
|
|
132
367
|
/**
|
|
133
368
|
* @name readLogoAsset
|
|
@@ -265,13 +500,27 @@ export function buildPublicServerInfo(req, config) {
|
|
|
265
500
|
},
|
|
266
501
|
endpoints: {
|
|
267
502
|
landing: `${baseUrl}/`,
|
|
503
|
+
docs: `${baseUrl}/docs`,
|
|
268
504
|
mcp: `${baseUrl}/mcp`,
|
|
269
505
|
health: `${baseUrl}/health`,
|
|
270
506
|
serverInfo: `${baseUrl}/server.json`,
|
|
507
|
+
openApi: `${baseUrl}/openapi.json`,
|
|
508
|
+
x402Discovery: `${baseUrl}/.well-known/x402`,
|
|
509
|
+
payShProvider: `${baseUrl}/pay/provider.yml`,
|
|
271
510
|
agentCard: `${baseUrl}/.well-known/agent-card.json`,
|
|
272
511
|
wizardDescriptor: `${baseUrl}/.well-known/sap-mcp-wizard.json`,
|
|
512
|
+
wizardDownloads: `${baseUrl}/wizard/downloads.json`,
|
|
273
513
|
wizardInstallScript: `${baseUrl}/wizard/install.sh`,
|
|
274
514
|
favicon: `${baseUrl}/favicon.png`,
|
|
515
|
+
faviconIco: `${baseUrl}/favicon.ico`,
|
|
516
|
+
},
|
|
517
|
+
downloads: {
|
|
518
|
+
release: `https://github.com/OOBE-PROTOCOL/sap-mcp/releases/tag/${MCP_SERVER_VERSION}`,
|
|
519
|
+
desktopWizard: {
|
|
520
|
+
macosArm64Dmg: `${RELEASE_DOWNLOAD_BASE_URL}/SAP-MCP-Wizard-${MCP_SERVER_VERSION}-arm64.dmg`,
|
|
521
|
+
windowsX64Setup: `${RELEASE_DOWNLOAD_BASE_URL}/SAP-MCP-Wizard-Setup-${MCP_SERVER_VERSION}-x64.exe`,
|
|
522
|
+
linuxX64TarGz: `${RELEASE_DOWNLOAD_BASE_URL}/sap-mcp-wizard-${MCP_SERVER_VERSION}-x64.tar.gz`,
|
|
523
|
+
},
|
|
275
524
|
},
|
|
276
525
|
capabilities: {
|
|
277
526
|
tools: true,
|
|
@@ -285,6 +534,7 @@ export function buildPublicServerInfo(req, config) {
|
|
|
285
534
|
schemes: authRequired ? ['Bearer'] : ['none', 'x402'],
|
|
286
535
|
bearerRequired: authRequired,
|
|
287
536
|
},
|
|
537
|
+
...(config.paymentDiscovery ? { payments: config.paymentDiscovery } : {}),
|
|
288
538
|
security: {
|
|
289
539
|
keypairBytesExposed: false,
|
|
290
540
|
storesUserKeypairs: false,
|
|
@@ -298,6 +548,237 @@ export function buildPublicServerInfo(req, config) {
|
|
|
298
548
|
},
|
|
299
549
|
};
|
|
300
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* @name buildPublicPayShProviderYaml
|
|
553
|
+
* @description Builds a public pay.sh provider catalog for the hosted SAP MCP endpoint without RPC secrets or signer paths.
|
|
554
|
+
*/
|
|
555
|
+
export function buildPublicPayShProviderYaml(req, config, appConfig) {
|
|
556
|
+
const baseUrl = buildPublicBaseUrl(req, config);
|
|
557
|
+
return generatePayShProviderYaml(appConfig, {
|
|
558
|
+
name: 'oobe-sap-mcp',
|
|
559
|
+
subdomain: 'oobe-sap-mcp',
|
|
560
|
+
title: 'OOBE SAP MCP Server',
|
|
561
|
+
description: PUBLIC_SERVER_DESCRIPTION,
|
|
562
|
+
version: MCP_SERVER_VERSION,
|
|
563
|
+
upstreamUrl: baseUrl,
|
|
564
|
+
recipient: config.paymentDiscovery?.payTo ?? appConfig.monetization.payTo,
|
|
565
|
+
rpcUrl: undefined,
|
|
566
|
+
signerPath: undefined,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* @name buildOpenApiSpec
|
|
571
|
+
* @description OpenAPI 3.1 spec for x402scan discovery. Describes the /mcp endpoint
|
|
572
|
+
* with x-payment-info so x402scan can discover and register the API.
|
|
573
|
+
*/
|
|
574
|
+
export function buildOpenApiSpec(req, config) {
|
|
575
|
+
const baseUrl = buildPublicBaseUrl(req, config);
|
|
576
|
+
const priceRange = config.paymentPriceRange ?? { minUsd: 0.001, maxUsd: 100 };
|
|
577
|
+
return {
|
|
578
|
+
openapi: '3.1.0',
|
|
579
|
+
info: {
|
|
580
|
+
title: 'SAP MCP Server',
|
|
581
|
+
version: MCP_SERVER_VERSION,
|
|
582
|
+
description: 'MCP gateway for Solana DeFi, SAP agent registry, and x402 micropayments. 248 tools across Jupiter, Drift, Metaplex, Pump.fun, Raydium, Orca, and more.',
|
|
583
|
+
'x-guidance': 'Connect to POST /mcp with Accept: application/json, text/event-stream. Initialize an MCP session first, then call tools. Paid tools return a payment_required JSON-RPC error (HTTP 200 for MCP SDK compatibility) or HTTP 402 for non-MCP clients. Use x402_paid_call or npx sap-mcp-x402-paid-call to self-pay and retry.',
|
|
584
|
+
contact: {
|
|
585
|
+
name: 'OOBE Protocol Labs',
|
|
586
|
+
url: 'https://github.com/OOBE-PROTOCOL/sap-mcp',
|
|
587
|
+
email: 'oobe@oobeprotocol.ai',
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
'x-discovery': {
|
|
591
|
+
resources: [`${baseUrl}/mcp`],
|
|
592
|
+
openApi: `${baseUrl}/openapi.json`,
|
|
593
|
+
x402Discovery: `${baseUrl}/.well-known/x402`,
|
|
594
|
+
payShProvider: `${baseUrl}/pay/provider.yml`,
|
|
595
|
+
...(config.paymentDiscovery ? { payments: config.paymentDiscovery } : {}),
|
|
596
|
+
},
|
|
597
|
+
'x-pay-sh': {
|
|
598
|
+
providerYaml: `${baseUrl}/pay/provider.yml`,
|
|
599
|
+
routing: {
|
|
600
|
+
type: 'proxy',
|
|
601
|
+
upstream: `${baseUrl}/`,
|
|
602
|
+
},
|
|
603
|
+
monetization: {
|
|
604
|
+
mode: 'metered',
|
|
605
|
+
unit: 'requests',
|
|
606
|
+
sourceOfTruth: 'SAP MCP per-tool x402 pricing remains authoritative after proxy forwarding.',
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
servers: [
|
|
610
|
+
{ url: baseUrl, description: 'SAP MCP Hosted Server' },
|
|
611
|
+
],
|
|
612
|
+
paths: {
|
|
613
|
+
'/mcp': {
|
|
614
|
+
post: {
|
|
615
|
+
operationId: 'mcpCall',
|
|
616
|
+
summary: 'Fetch SAP MCP JSON-RPC response',
|
|
617
|
+
tags: ['MCP'],
|
|
618
|
+
'x-payment-info': {
|
|
619
|
+
price: {
|
|
620
|
+
mode: 'dynamic',
|
|
621
|
+
currency: 'USD',
|
|
622
|
+
min: formatOpenApiUsd(priceRange.minUsd),
|
|
623
|
+
max: formatOpenApiUsd(priceRange.maxUsd),
|
|
624
|
+
},
|
|
625
|
+
protocols: ['x402'],
|
|
626
|
+
...(config.paymentDiscovery
|
|
627
|
+
? {
|
|
628
|
+
network: config.paymentDiscovery.network,
|
|
629
|
+
asset: config.paymentDiscovery.asset,
|
|
630
|
+
assetSymbol: config.paymentDiscovery.assetSymbol,
|
|
631
|
+
payTo: config.paymentDiscovery.payTo,
|
|
632
|
+
seller: config.paymentDiscovery.seller,
|
|
633
|
+
facilitator: config.paymentDiscovery.facilitator,
|
|
634
|
+
}
|
|
635
|
+
: {}),
|
|
636
|
+
},
|
|
637
|
+
requestBody: {
|
|
638
|
+
required: true,
|
|
639
|
+
content: {
|
|
640
|
+
'application/json': {
|
|
641
|
+
schema: {
|
|
642
|
+
oneOf: [
|
|
643
|
+
{
|
|
644
|
+
type: 'object',
|
|
645
|
+
description: 'General MCP JSON-RPC request such as initialize, tools/list, prompts/list, resources/list, or ping.',
|
|
646
|
+
properties: {
|
|
647
|
+
jsonrpc: { type: 'string', const: '2.0' },
|
|
648
|
+
id: { oneOf: [{ type: 'integer' }, { type: 'string' }], description: 'JSON-RPC request ID' },
|
|
649
|
+
method: {
|
|
650
|
+
type: 'string',
|
|
651
|
+
description: 'MCP method.',
|
|
652
|
+
examples: ['initialize', 'tools/list', 'prompts/list', 'resources/list', 'ping'],
|
|
653
|
+
},
|
|
654
|
+
params: { type: 'object', additionalProperties: true },
|
|
655
|
+
},
|
|
656
|
+
required: ['jsonrpc', 'id', 'method'],
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
type: 'object',
|
|
660
|
+
description: 'MCP tools/call request. Paid tool calls may require x402 payment before execution.',
|
|
661
|
+
properties: {
|
|
662
|
+
jsonrpc: { type: 'string', const: '2.0' },
|
|
663
|
+
id: { oneOf: [{ type: 'integer' }, { type: 'string' }], description: 'JSON-RPC request ID' },
|
|
664
|
+
method: { type: 'string', const: 'tools/call' },
|
|
665
|
+
params: {
|
|
666
|
+
type: 'object',
|
|
667
|
+
description: 'MCP tools/call parameters containing the target tool name and optional tool arguments.',
|
|
668
|
+
properties: {
|
|
669
|
+
name: { type: 'string', description: 'Tool name (e.g. jupiter_getPrice)' },
|
|
670
|
+
arguments: { type: 'object', description: 'Tool arguments', additionalProperties: true },
|
|
671
|
+
},
|
|
672
|
+
required: ['name'],
|
|
673
|
+
additionalProperties: true,
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
required: ['jsonrpc', 'id', 'method', 'params'],
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
responses: {
|
|
684
|
+
'200': {
|
|
685
|
+
description: 'MCP tool result or payment_required JSON-RPC error (for MCP SDK clients)',
|
|
686
|
+
content: {
|
|
687
|
+
'application/json': {
|
|
688
|
+
schema: {
|
|
689
|
+
type: 'object',
|
|
690
|
+
properties: {
|
|
691
|
+
jsonrpc: { type: 'string' },
|
|
692
|
+
id: { type: 'integer' },
|
|
693
|
+
result: { type: 'object', description: 'Tool result (if free or paid)' },
|
|
694
|
+
error: {
|
|
695
|
+
type: 'object',
|
|
696
|
+
properties: {
|
|
697
|
+
code: { type: 'integer' },
|
|
698
|
+
message: { type: 'string' },
|
|
699
|
+
data: {
|
|
700
|
+
type: 'object',
|
|
701
|
+
properties: {
|
|
702
|
+
protocol: { type: 'string' },
|
|
703
|
+
paymentRequirements: { type: 'array' },
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
'text/event-stream': {
|
|
712
|
+
schema: {
|
|
713
|
+
type: 'string',
|
|
714
|
+
description: 'Streamable HTTP MCP event stream containing JSON-RPC response events.',
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
'402': {
|
|
720
|
+
description: 'Payment Required (x402 challenge for non-MCP clients)',
|
|
721
|
+
content: {
|
|
722
|
+
'application/json': {
|
|
723
|
+
schema: {
|
|
724
|
+
type: 'object',
|
|
725
|
+
properties: {
|
|
726
|
+
x402Version: { type: 'integer' },
|
|
727
|
+
error: { type: 'string' },
|
|
728
|
+
accepts: {
|
|
729
|
+
type: 'array',
|
|
730
|
+
items: {
|
|
731
|
+
type: 'object',
|
|
732
|
+
properties: {
|
|
733
|
+
scheme: { type: 'string' },
|
|
734
|
+
network: { type: 'string' },
|
|
735
|
+
amount: { type: 'string', description: 'USDC atomic units (6 decimals)' },
|
|
736
|
+
asset: { type: 'string' },
|
|
737
|
+
payTo: { type: 'string' },
|
|
738
|
+
maxTimeoutSeconds: { type: 'integer' },
|
|
739
|
+
extra: {
|
|
740
|
+
type: 'object',
|
|
741
|
+
properties: {
|
|
742
|
+
feePayer: { type: 'string' },
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
resource: {
|
|
749
|
+
type: 'object',
|
|
750
|
+
properties: {
|
|
751
|
+
url: { type: 'string' },
|
|
752
|
+
description: { type: 'string' },
|
|
753
|
+
mimeType: { type: 'string' },
|
|
754
|
+
serviceName: { type: 'string' },
|
|
755
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* @name buildX402DiscoveryDocument
|
|
771
|
+
* @description Builds the compatibility discovery document consumed by x402scan.
|
|
772
|
+
*/
|
|
773
|
+
export function buildX402DiscoveryDocument(req, config) {
|
|
774
|
+
const baseUrl = buildPublicBaseUrl(req, config);
|
|
775
|
+
return {
|
|
776
|
+
version: 1,
|
|
777
|
+
resources: [`${baseUrl}/mcp`],
|
|
778
|
+
...(config.paymentDiscovery ? { payments: config.paymentDiscovery } : {}),
|
|
779
|
+
instructions: 'SAP MCP exposes paid hosted MCP tool calls at /mcp. Probe POST /mcp without a payment header to receive a parseable x402 challenge for paid tools.',
|
|
780
|
+
};
|
|
781
|
+
}
|
|
301
782
|
/**
|
|
302
783
|
* @name buildLandingHtml
|
|
303
784
|
* @description Builds professional public HTML metadata for root and `/mcp` URL previews.
|
|
@@ -310,8 +791,9 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
|
|
|
310
791
|
? 'SAP MCP Streamable HTTP Endpoint | OOBE Protocol'
|
|
311
792
|
: info.title;
|
|
312
793
|
const endpointLabel = endpoint === 'mcp' ? 'Streamable HTTP MCP endpoint' : 'Hosted MCP gateway';
|
|
313
|
-
const wizardCommand =
|
|
794
|
+
const wizardCommand = WIZARD_NPM_COMMAND;
|
|
314
795
|
const installScriptCommand = 'curl -fsSL https://mcp.sap.oobeprotocol.ai/wizard/install.sh | sh';
|
|
796
|
+
const x402PaidCallCommand = X402_PAID_CALL_NPX_COMMAND;
|
|
315
797
|
const publicInfo = JSON.stringify(info, null, 2).replace(/</g, '\\u003c');
|
|
316
798
|
return `<!doctype html>
|
|
317
799
|
<html lang="en">
|
|
@@ -322,8 +804,9 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
|
|
|
322
804
|
<meta name="description" content="${escapeHtml(info.description)}">
|
|
323
805
|
<meta name="robots" content="index,follow">
|
|
324
806
|
<link rel="canonical" href="${escapeHtml(pageUrl)}">
|
|
807
|
+
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
|
325
808
|
<link rel="icon" type="image/png" href="/favicon.png">
|
|
326
|
-
<link rel="shortcut icon"
|
|
809
|
+
<link rel="shortcut icon" href="/favicon.ico">
|
|
327
810
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
328
811
|
<meta property="og:type" content="website">
|
|
329
812
|
<meta property="og:title" content="${escapeHtml(title)}">
|
|
@@ -428,6 +911,21 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
|
|
|
428
911
|
.list { padding-left: 18px; margin: 0; color: var(--muted); }
|
|
429
912
|
.list li + li { margin-top: 8px; }
|
|
430
913
|
.command { margin-top: 12px; border: 1px solid rgba(255,255,255,.10); border-radius: 8px; background: rgba(0,0,0,.28); padding: 13px; }
|
|
914
|
+
.download-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 14px; }
|
|
915
|
+
.download-link {
|
|
916
|
+
display: grid;
|
|
917
|
+
min-height: 74px;
|
|
918
|
+
align-content: center;
|
|
919
|
+
gap: 4px;
|
|
920
|
+
border: 1px solid var(--line);
|
|
921
|
+
border-radius: 8px;
|
|
922
|
+
background: rgba(0,0,0,.18);
|
|
923
|
+
padding: 12px;
|
|
924
|
+
color: var(--ink);
|
|
925
|
+
text-decoration: none;
|
|
926
|
+
}
|
|
927
|
+
.download-link strong { color: var(--aqua); }
|
|
928
|
+
.download-link span { color: var(--muted); font-size: .88rem; }
|
|
431
929
|
.endpoint-list { display: grid; gap: 10px; margin-top: 10px; }
|
|
432
930
|
.endpoint-row { display: grid; grid-template-columns: 72px 1fr; gap: 12px; align-items: baseline; }
|
|
433
931
|
.method { color: var(--success); font: 700 .78rem/1 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
@@ -436,6 +934,7 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
|
|
|
436
934
|
header { grid-template-columns: 1fr; }
|
|
437
935
|
img.logo { width: 76px; height: 76px; border-radius: 20px; }
|
|
438
936
|
.span-3, .span-4, .span-5, .span-6, .span-7, .span-8 { grid-column: span 12; }
|
|
937
|
+
.download-grid { grid-template-columns: 1fr; }
|
|
439
938
|
}
|
|
440
939
|
@media (min-width: 861px) and (max-width: 1120px) {
|
|
441
940
|
.span-3, .span-4 { grid-column: span 6; }
|
|
@@ -456,7 +955,11 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
|
|
|
456
955
|
<p>${escapeHtml(info.description)}</p>
|
|
457
956
|
<div class="hero-actions">
|
|
458
957
|
<a class="button" href="/wizard/install.sh">Install wizard</a>
|
|
958
|
+
<a class="button secondary" href="/wizard/downloads.json">Downloads</a>
|
|
959
|
+
<a class="button secondary" href="/docs">Docs</a>
|
|
459
960
|
<a class="button secondary" href="/server.json">Server JSON</a>
|
|
961
|
+
<a class="button secondary" href="/openapi.json">OpenAPI</a>
|
|
962
|
+
<a class="button secondary" href="/pay/provider.yml">pay.sh YAML</a>
|
|
460
963
|
<a class="button secondary" href="/.well-known/agent-card.json">Agent card</a>
|
|
461
964
|
</div>
|
|
462
965
|
</div>
|
|
@@ -488,21 +991,43 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
|
|
|
488
991
|
<div class="command"><code>${escapeHtml(wizardCommand)}</code></div>
|
|
489
992
|
</section>
|
|
490
993
|
|
|
994
|
+
<section class="card span-5">
|
|
995
|
+
<h2>Native Downloads</h2>
|
|
996
|
+
<p>Use the desktop wizard when you want a guided setup without touching config files by hand.</p>
|
|
997
|
+
<div class="download-grid">
|
|
998
|
+
<a class="download-link" href="${escapeHtml(info.downloads.desktopWizard.windowsX64Setup)}"><strong>Windows</strong><span>x64 setup .exe</span></a>
|
|
999
|
+
<a class="download-link" href="${escapeHtml(info.downloads.desktopWizard.macosArm64Dmg)}"><strong>macOS</strong><span>Apple Silicon .dmg</span></a>
|
|
1000
|
+
<a class="download-link" href="${escapeHtml(info.downloads.desktopWizard.linuxX64TarGz)}"><strong>Linux</strong><span>x64 tar.gz</span></a>
|
|
1001
|
+
</div>
|
|
1002
|
+
<p class="caption">Machine-readable links: <a href="${escapeHtml(info.endpoints.wizardDownloads)}">/wizard/downloads.json</a>.</p>
|
|
1003
|
+
</section>
|
|
1004
|
+
|
|
491
1005
|
<section class="card span-5">
|
|
492
1006
|
<h2>Payments</h2>
|
|
493
1007
|
<ul class="list">
|
|
494
1008
|
<li><strong>x402:</strong> paid MCP tool calls return HTTP 402 with payment requirements, then settle through the OOBE facilitator.</li>
|
|
495
|
-
<li><strong>pay.sh:</strong>
|
|
1009
|
+
<li><strong>pay.sh:</strong> public provider YAML is available at <a href="${escapeHtml(info.endpoints.payShProvider)}">/pay/provider.yml</a> for catalog and proxy workflows.</li>
|
|
496
1010
|
<li><strong>Local stdio:</strong> free by default; monetization applies to hosted remote usage.</li>
|
|
497
1011
|
</ul>
|
|
498
1012
|
</section>
|
|
499
1013
|
|
|
1014
|
+
<section class="card strong span-12">
|
|
1015
|
+
<h2>x402 Paid-Call Plugin For Agents</h2>
|
|
1016
|
+
<p>Agents that can reach hosted MCP but cannot replay x402 challenges natively can install the wizard addon or invoke the local paid-call helper directly. The helper signs the payment payload locally and retries the hosted tool call without sending keypair bytes to OOBE.</p>
|
|
1017
|
+
<div class="command"><code>Addon path: ${escapeHtml(X402_PAID_CALL_ADDON_PATH)}</code></div>
|
|
1018
|
+
<div class="command"><code>${escapeHtml(x402PaidCallCommand)}</code></div>
|
|
1019
|
+
</section>
|
|
1020
|
+
|
|
500
1021
|
<section class="card span-8">
|
|
501
1022
|
<h2>Endpoints</h2>
|
|
502
1023
|
<div class="endpoint-list">
|
|
503
1024
|
<div class="endpoint-row"><span class="method">POST</span><code>${escapeHtml(info.endpoints.mcp)}</code></div>
|
|
1025
|
+
<div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.docs)}</code></div>
|
|
504
1026
|
<div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.health)}</code></div>
|
|
505
1027
|
<div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.serverInfo)}</code></div>
|
|
1028
|
+
<div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.openApi)}</code></div>
|
|
1029
|
+
<div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.x402Discovery)}</code></div>
|
|
1030
|
+
<div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.payShProvider)}</code></div>
|
|
506
1031
|
<div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.agentCard)}</code></div>
|
|
507
1032
|
<div class="endpoint-row"><span class="method">GET</span><code>${escapeHtml(info.endpoints.wizardDescriptor)}</code></div>
|
|
508
1033
|
</div>
|
|
@@ -522,6 +1047,115 @@ export function buildLandingHtml(req, config, endpoint = 'root') {
|
|
|
522
1047
|
</body>
|
|
523
1048
|
</html>`;
|
|
524
1049
|
}
|
|
1050
|
+
/**
|
|
1051
|
+
* @name buildDocsHtml
|
|
1052
|
+
* @description Builds the hosted SAP MCP documentation shell powered by Docsify.
|
|
1053
|
+
*/
|
|
1054
|
+
export function buildDocsHtml(req, config) {
|
|
1055
|
+
const info = buildPublicServerInfo(req, config);
|
|
1056
|
+
const docsUrl = info.endpoints.docs;
|
|
1057
|
+
const title = 'SAP MCP Documentation | OOBE Protocol';
|
|
1058
|
+
const description = 'Install, configure, and operate SAP MCP with a focus on hosted remote access, local signer setup, x402/pay.sh payments, and MCP client configuration.';
|
|
1059
|
+
const docsifyConfig = JSON.stringify({
|
|
1060
|
+
name: 'SAP MCP',
|
|
1061
|
+
repo: 'https://github.com/OOBE-PROTOCOL/sap-mcp',
|
|
1062
|
+
homepage: 'README.md',
|
|
1063
|
+
loadSidebar: true,
|
|
1064
|
+
alias: {
|
|
1065
|
+
'/.*/_sidebar.md': '/docs/_sidebar.md',
|
|
1066
|
+
},
|
|
1067
|
+
subMaxLevel: 2,
|
|
1068
|
+
auto2top: true,
|
|
1069
|
+
themeColor: '#28d8e8',
|
|
1070
|
+
search: {
|
|
1071
|
+
paths: 'auto',
|
|
1072
|
+
placeholder: 'Search SAP MCP docs',
|
|
1073
|
+
noData: 'No results.',
|
|
1074
|
+
depth: 6,
|
|
1075
|
+
},
|
|
1076
|
+
}, null, 2).replace(/</g, '\\u003c');
|
|
1077
|
+
return `<!doctype html>
|
|
1078
|
+
<html lang="en">
|
|
1079
|
+
<head>
|
|
1080
|
+
<meta charset="utf-8">
|
|
1081
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1082
|
+
<title>${escapeHtml(title)}</title>
|
|
1083
|
+
<meta name="description" content="${escapeHtml(description)}">
|
|
1084
|
+
<meta name="robots" content="index,follow">
|
|
1085
|
+
<link rel="canonical" href="${escapeHtml(docsUrl)}">
|
|
1086
|
+
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
|
1087
|
+
<link rel="icon" type="image/png" href="/favicon.png">
|
|
1088
|
+
<link rel="shortcut icon" href="/favicon.ico">
|
|
1089
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
1090
|
+
<meta property="og:type" content="website">
|
|
1091
|
+
<meta property="og:title" content="${escapeHtml(title)}">
|
|
1092
|
+
<meta property="og:description" content="${escapeHtml(description)}">
|
|
1093
|
+
<meta property="og:url" content="${escapeHtml(docsUrl)}">
|
|
1094
|
+
<meta property="og:image" content="${escapeHtml(info.endpoints.favicon)}">
|
|
1095
|
+
<meta name="twitter:card" content="summary">
|
|
1096
|
+
<meta name="twitter:title" content="${escapeHtml(title)}">
|
|
1097
|
+
<meta name="twitter:description" content="${escapeHtml(description)}">
|
|
1098
|
+
<meta name="twitter:image" content="${escapeHtml(info.endpoints.favicon)}">
|
|
1099
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsify@4.13.1/lib/themes/vue.css">
|
|
1100
|
+
<style>
|
|
1101
|
+
:root {
|
|
1102
|
+
--theme-color: #28d8e8;
|
|
1103
|
+
--sidebar-width: 310px;
|
|
1104
|
+
}
|
|
1105
|
+
body {
|
|
1106
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1107
|
+
color: #17242b;
|
|
1108
|
+
}
|
|
1109
|
+
.app-name-link {
|
|
1110
|
+
color: #062730;
|
|
1111
|
+
font-weight: 800;
|
|
1112
|
+
}
|
|
1113
|
+
.sidebar {
|
|
1114
|
+
border-right: 1px solid rgba(6,39,48,.10);
|
|
1115
|
+
}
|
|
1116
|
+
.sidebar-nav strong,
|
|
1117
|
+
.markdown-section h1,
|
|
1118
|
+
.markdown-section h2,
|
|
1119
|
+
.markdown-section h3 {
|
|
1120
|
+
color: #062730;
|
|
1121
|
+
letter-spacing: 0;
|
|
1122
|
+
}
|
|
1123
|
+
.markdown-section a {
|
|
1124
|
+
color: #087f91;
|
|
1125
|
+
text-underline-offset: 3px;
|
|
1126
|
+
}
|
|
1127
|
+
.markdown-section code {
|
|
1128
|
+
color: #062730;
|
|
1129
|
+
border-radius: 6px;
|
|
1130
|
+
}
|
|
1131
|
+
.markdown-section pre {
|
|
1132
|
+
border-radius: 8px;
|
|
1133
|
+
border: 1px solid rgba(6,39,48,.10);
|
|
1134
|
+
}
|
|
1135
|
+
.markdown-section table {
|
|
1136
|
+
display: table;
|
|
1137
|
+
width: 100%;
|
|
1138
|
+
}
|
|
1139
|
+
.markdown-section blockquote {
|
|
1140
|
+
border-left-color: #28d8e8;
|
|
1141
|
+
color: #49656f;
|
|
1142
|
+
}
|
|
1143
|
+
.cover-main,
|
|
1144
|
+
section.cover {
|
|
1145
|
+
background: linear-gradient(115deg, rgba(40,216,232,.13), transparent 34%), #f8fdff;
|
|
1146
|
+
}
|
|
1147
|
+
</style>
|
|
1148
|
+
</head>
|
|
1149
|
+
<body>
|
|
1150
|
+
<div id="app">Loading SAP MCP documentation...</div>
|
|
1151
|
+
<script>
|
|
1152
|
+
window.$docsify = ${docsifyConfig};
|
|
1153
|
+
</script>
|
|
1154
|
+
<script src="https://cdn.jsdelivr.net/npm/docsify@4.13.1/lib/docsify.min.js" defer></script>
|
|
1155
|
+
<script src="https://cdn.jsdelivr.net/npm/docsify@4.13.1/lib/plugins/search.min.js" defer></script>
|
|
1156
|
+
</body>
|
|
1157
|
+
</html>`;
|
|
1158
|
+
}
|
|
525
1159
|
/**
|
|
526
1160
|
* @name acceptsHtmlPreview
|
|
527
1161
|
* @description Returns true for browser-style GET previews without interfering with MCP SSE clients.
|
|
@@ -662,13 +1296,16 @@ export function buildWizardInstallDescriptor(req, config) {
|
|
|
662
1296
|
],
|
|
663
1297
|
endpoints: {
|
|
664
1298
|
descriptor: `${baseUrl}/.well-known/sap-mcp-wizard.json`,
|
|
1299
|
+
downloads: `${baseUrl}/wizard/downloads.json`,
|
|
665
1300
|
installScript: `${baseUrl}/wizard/install.sh`,
|
|
666
1301
|
mcp: `${baseUrl}/mcp`,
|
|
667
1302
|
},
|
|
668
1303
|
commands: {
|
|
669
|
-
runWizard:
|
|
1304
|
+
runWizard: WIZARD_NPM_COMMAND,
|
|
670
1305
|
showConfig: 'npm exec --yes --package @oobe-protocol-labs/sap-mcp-server -- sap-mcp-config show',
|
|
671
1306
|
showProfile: 'npm exec --yes --package @oobe-protocol-labs/sap-mcp-server -- sap-mcp-config profile-info',
|
|
1307
|
+
installX402PaidCallAddon: WIZARD_NPM_COMMAND,
|
|
1308
|
+
runX402PaidCall: X402_PAID_CALL_NPX_COMMAND,
|
|
672
1309
|
},
|
|
673
1310
|
security: {
|
|
674
1311
|
keypairBytesExposed: false,
|
|
@@ -700,6 +1337,44 @@ export function buildWizardInstallScript(req, config) {
|
|
|
700
1337
|
'',
|
|
701
1338
|
].join('\n');
|
|
702
1339
|
}
|
|
1340
|
+
/**
|
|
1341
|
+
* @name extractToolNamesFromJsonRpc
|
|
1342
|
+
* @description Extracts tool names from a parsed JSON-RPC 2.0 request body for request logging.
|
|
1343
|
+
*/
|
|
1344
|
+
function extractToolNamesFromJsonRpc(body) {
|
|
1345
|
+
if (!body || typeof body !== 'object')
|
|
1346
|
+
return undefined;
|
|
1347
|
+
const record = body;
|
|
1348
|
+
// tools/call request: { method: "tools/call", params: { name: "tool_name", ... } }
|
|
1349
|
+
if (record.method === 'tools/call' && record.params && typeof record.params === 'object') {
|
|
1350
|
+
const params = record.params;
|
|
1351
|
+
if (typeof params.name === 'string')
|
|
1352
|
+
return [params.name];
|
|
1353
|
+
}
|
|
1354
|
+
// Batch request: array of requests
|
|
1355
|
+
if (Array.isArray(body)) {
|
|
1356
|
+
const names = [];
|
|
1357
|
+
for (const item of body) {
|
|
1358
|
+
const extracted = extractToolNamesFromJsonRpc(item);
|
|
1359
|
+
if (extracted)
|
|
1360
|
+
names.push(...extracted);
|
|
1361
|
+
}
|
|
1362
|
+
return names.length > 0 ? names : undefined;
|
|
1363
|
+
}
|
|
1364
|
+
return undefined;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* @name extractJsonRpcMethod
|
|
1368
|
+
* @description Extracts the JSON-RPC method name from a parsed request body for request logging.
|
|
1369
|
+
*/
|
|
1370
|
+
function extractJsonRpcMethod(body) {
|
|
1371
|
+
if (!body || typeof body !== 'object')
|
|
1372
|
+
return undefined;
|
|
1373
|
+
const record = body;
|
|
1374
|
+
if (typeof record.method === 'string')
|
|
1375
|
+
return record.method;
|
|
1376
|
+
return undefined;
|
|
1377
|
+
}
|
|
703
1378
|
/**
|
|
704
1379
|
* @name RemoteMCPServer
|
|
705
1380
|
* @description Remote MCP server backed by the official Streamable HTTP transport.
|
|
@@ -709,7 +1384,7 @@ export class RemoteMCPServer {
|
|
|
709
1384
|
appConfig;
|
|
710
1385
|
authManager;
|
|
711
1386
|
rateLimiter;
|
|
712
|
-
|
|
1387
|
+
statefulSessions = new Map();
|
|
713
1388
|
httpServer;
|
|
714
1389
|
constructor(config) {
|
|
715
1390
|
this.appConfig = loadConfig();
|
|
@@ -730,63 +1405,88 @@ export class RemoteMCPServer {
|
|
|
730
1405
|
this.validateAuthConfig();
|
|
731
1406
|
await this.rateLimiter.initialize();
|
|
732
1407
|
const monetizationGate = await McpMonetizationGate.create(this.appConfig);
|
|
733
|
-
// In stateful mode, create one long-lived transport and connect the server once.
|
|
734
|
-
// In stateless mode, a fresh transport + server must be created per request —
|
|
735
|
-
// the MCP SDK throws "Stateless transport cannot be reused across requests"
|
|
736
|
-
// if a stateless transport handles more than one request.
|
|
737
|
-
if (!this.config.stateless) {
|
|
738
|
-
const server = await createSapMcpServer(this.appConfig);
|
|
739
|
-
this.transport = new StreamableHTTPServerTransport({
|
|
740
|
-
sessionIdGenerator: () => randomUUID(),
|
|
741
|
-
});
|
|
742
|
-
await server.connect(this.transport);
|
|
743
|
-
}
|
|
744
1408
|
this.httpServer = http.createServer(async (req, res) => {
|
|
745
1409
|
if (this.applyCors(req, res)) {
|
|
746
1410
|
return;
|
|
747
1411
|
}
|
|
748
1412
|
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
749
|
-
if (req.method
|
|
750
|
-
writeHtml(res, 200, buildLandingHtml(req, this.config));
|
|
1413
|
+
if (isPublicReadMethod(req.method) && url.pathname === '/') {
|
|
1414
|
+
writeHtml(res, 200, buildLandingHtml(req, this.config), isHeadMethod(req.method));
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (isPublicReadMethod(req.method) && ['/docs', '/docs/', '/docs/index.html'].includes(url.pathname)) {
|
|
1418
|
+
writeHtml(res, 200, buildDocsHtml(req, this.config), isHeadMethod(req.method));
|
|
751
1419
|
return;
|
|
752
1420
|
}
|
|
753
|
-
if (req.method
|
|
1421
|
+
if (isPublicReadMethod(req.method) && url.pathname === '/server.json') {
|
|
754
1422
|
writeJson(res, 200, buildPublicServerInfo(req, this.config), {
|
|
755
1423
|
'Cache-Control': 'public, max-age=300',
|
|
756
|
-
});
|
|
1424
|
+
}, isHeadMethod(req.method));
|
|
757
1425
|
return;
|
|
758
1426
|
}
|
|
759
|
-
|
|
760
|
-
|
|
1427
|
+
const publicLogoAsset = resolvePublicLogoAsset(req.method, url.pathname);
|
|
1428
|
+
if (publicLogoAsset) {
|
|
1429
|
+
writeLogoAsset(res, publicLogoAsset);
|
|
761
1430
|
return;
|
|
762
1431
|
}
|
|
763
|
-
|
|
1432
|
+
const publicDocsMarkdown = resolvePublicDocsMarkdown(req.method, url.pathname);
|
|
1433
|
+
if (publicDocsMarkdown) {
|
|
1434
|
+
writeMarkdownAsset(res, publicDocsMarkdown);
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
if (isPublicReadMethod(req.method) && url.pathname === '/health') {
|
|
764
1438
|
writeJson(res, 200, {
|
|
765
1439
|
status: 'ok',
|
|
766
1440
|
service: 'sap-mcp-remote',
|
|
767
1441
|
transport: 'streamable-http',
|
|
768
1442
|
timestamp: new Date().toISOString(),
|
|
769
|
-
});
|
|
1443
|
+
}, {}, isHeadMethod(req.method));
|
|
770
1444
|
return;
|
|
771
1445
|
}
|
|
772
|
-
if (req.method
|
|
773
|
-
writeJson(res, 200, buildA2AAgentCard(req, this.config));
|
|
1446
|
+
if (isPublicReadMethod(req.method) && ['/.well-known/agent-card.json', '/a2a/agent-card'].includes(url.pathname)) {
|
|
1447
|
+
writeJson(res, 200, buildA2AAgentCard(req, this.config), {}, isHeadMethod(req.method));
|
|
774
1448
|
return;
|
|
775
1449
|
}
|
|
776
|
-
if (req.method
|
|
777
|
-
writeJson(res, 200, buildWizardInstallDescriptor(req, this.config));
|
|
1450
|
+
if (isPublicReadMethod(req.method) && ['/.well-known/sap-mcp-wizard.json', '/wizard', '/wizard.json'].includes(url.pathname)) {
|
|
1451
|
+
writeJson(res, 200, buildWizardInstallDescriptor(req, this.config), {}, isHeadMethod(req.method));
|
|
778
1452
|
return;
|
|
779
1453
|
}
|
|
780
|
-
if (req.method
|
|
1454
|
+
if (isPublicReadMethod(req.method) && url.pathname === '/wizard/downloads.json') {
|
|
1455
|
+
const info = buildPublicServerInfo(req, this.config);
|
|
1456
|
+
writeJson(res, 200, info.downloads, {
|
|
1457
|
+
'Cache-Control': 'public, max-age=300',
|
|
1458
|
+
}, isHeadMethod(req.method));
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
if (isPublicReadMethod(req.method) && url.pathname === '/openapi.json') {
|
|
1462
|
+
writeJson(res, 200, buildOpenApiSpec(req, this.config), {
|
|
1463
|
+
'Cache-Control': 'public, max-age=300',
|
|
1464
|
+
}, isHeadMethod(req.method));
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
if (isPublicReadMethod(req.method) && url.pathname === '/pay/provider.yml') {
|
|
1468
|
+
const providerYaml = buildPublicPayShProviderYaml(req, this.config, this.appConfig);
|
|
1469
|
+
writeText(res, 200, providerYaml, 'application/yaml; charset=utf-8', {}, isHeadMethod(req.method));
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
if (isPublicReadMethod(req.method) && url.pathname === '/.well-known/x402') {
|
|
1473
|
+
writeJson(res, 200, buildX402DiscoveryDocument(req, this.config), {
|
|
1474
|
+
'Cache-Control': 'public, max-age=300',
|
|
1475
|
+
}, isHeadMethod(req.method));
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (isPublicReadMethod(req.method) && url.pathname === '/wizard/install.sh') {
|
|
1479
|
+
const script = buildWizardInstallScript(req, this.config);
|
|
781
1480
|
res.writeHead(200, {
|
|
782
1481
|
'Content-Type': 'text/x-shellscript; charset=utf-8',
|
|
783
1482
|
'Cache-Control': 'public, max-age=300',
|
|
1483
|
+
'Content-Length': Buffer.byteLength(script),
|
|
784
1484
|
});
|
|
785
|
-
res.end(
|
|
1485
|
+
res.end(isHeadMethod(req.method) ? undefined : script);
|
|
786
1486
|
return;
|
|
787
1487
|
}
|
|
788
|
-
if (req.method
|
|
789
|
-
writeHtml(res, 200, buildLandingHtml(req, this.config, 'mcp'));
|
|
1488
|
+
if (isPublicReadMethod(req.method) && url.pathname === '/mcp' && (isHeadMethod(req.method) || acceptsHtmlPreview(req))) {
|
|
1489
|
+
writeHtml(res, 200, buildLandingHtml(req, this.config, 'mcp'), isHeadMethod(req.method));
|
|
790
1490
|
return;
|
|
791
1491
|
}
|
|
792
1492
|
if (url.pathname !== '/mcp') {
|
|
@@ -813,6 +1513,26 @@ export class RemoteMCPServer {
|
|
|
813
1513
|
});
|
|
814
1514
|
return;
|
|
815
1515
|
}
|
|
1516
|
+
// ── Request logging ─────────────────────────────────────────────
|
|
1517
|
+
const reqStart = Date.now();
|
|
1518
|
+
const callerIp = getRateLimitKey(req);
|
|
1519
|
+
const originalEnd = res.end.bind(res);
|
|
1520
|
+
let loggedMethod;
|
|
1521
|
+
let loggedToolNames;
|
|
1522
|
+
res.end = ((...args) => {
|
|
1523
|
+
const latencyMs = Date.now() - reqStart;
|
|
1524
|
+
const statusCode = res.statusCode;
|
|
1525
|
+
logger.info('MCP request', {
|
|
1526
|
+
method: req.method,
|
|
1527
|
+
path: url.pathname,
|
|
1528
|
+
status: statusCode,
|
|
1529
|
+
latencyMs,
|
|
1530
|
+
callerIp,
|
|
1531
|
+
toolNames: loggedToolNames,
|
|
1532
|
+
rpcMethod: loggedMethod,
|
|
1533
|
+
});
|
|
1534
|
+
return originalEnd(...args);
|
|
1535
|
+
});
|
|
816
1536
|
try {
|
|
817
1537
|
if (this.config.stateless) {
|
|
818
1538
|
// Stateless mode: create a fresh transport + server per request.
|
|
@@ -824,22 +1544,32 @@ export class RemoteMCPServer {
|
|
|
824
1544
|
await perRequestServer.connect(perRequestTransport);
|
|
825
1545
|
if (monetizationGate) {
|
|
826
1546
|
await monetizationGate.handle(req, res, async (mcpReq, mcpRes, parsedBody) => {
|
|
1547
|
+
// Extract tool names from parsed JSON-RPC body for logging
|
|
1548
|
+
loggedToolNames = extractToolNamesFromJsonRpc(parsedBody);
|
|
1549
|
+
loggedMethod = extractJsonRpcMethod(parsedBody);
|
|
827
1550
|
await perRequestTransport.handleRequest(mcpReq, mcpRes, parsedBody);
|
|
828
1551
|
});
|
|
829
1552
|
}
|
|
830
1553
|
else {
|
|
1554
|
+
// Parse body for logging before passing to transport
|
|
1555
|
+
// The transport will re-read it from the stream
|
|
1556
|
+
loggedToolNames = undefined; // Will be logged without tool names in free mode
|
|
831
1557
|
await perRequestTransport.handleRequest(req, res);
|
|
832
1558
|
}
|
|
833
1559
|
}
|
|
834
1560
|
else {
|
|
835
|
-
// Stateful mode:
|
|
1561
|
+
// Stateful mode: route each request to the transport created by its initialize response.
|
|
836
1562
|
if (monetizationGate) {
|
|
837
1563
|
await monetizationGate.handle(req, res, async (mcpReq, mcpRes, parsedBody) => {
|
|
838
|
-
|
|
1564
|
+
loggedToolNames = extractToolNamesFromJsonRpc(parsedBody);
|
|
1565
|
+
loggedMethod = extractJsonRpcMethod(parsedBody);
|
|
1566
|
+
await this.handleStatefulMcpRequest(mcpReq, mcpRes, parsedBody);
|
|
1567
|
+
}, {
|
|
1568
|
+
validatePaidRequest: context => this.validateStatefulPaidRequest(context.request, context.parsedBody),
|
|
839
1569
|
});
|
|
840
1570
|
}
|
|
841
1571
|
else {
|
|
842
|
-
await this.
|
|
1572
|
+
await this.handleStatefulMcpRequest(req, res);
|
|
843
1573
|
}
|
|
844
1574
|
}
|
|
845
1575
|
}
|
|
@@ -878,7 +1608,9 @@ export class RemoteMCPServer {
|
|
|
878
1608
|
* @description Gracefully closes the MCP transport and HTTP listener.
|
|
879
1609
|
*/
|
|
880
1610
|
async stop() {
|
|
881
|
-
|
|
1611
|
+
const sessions = Array.from(this.statefulSessions.values());
|
|
1612
|
+
this.statefulSessions.clear();
|
|
1613
|
+
await Promise.allSettled(sessions.map(session => session.transport.close()));
|
|
882
1614
|
if (!this.httpServer) {
|
|
883
1615
|
return;
|
|
884
1616
|
}
|
|
@@ -906,6 +1638,110 @@ export class RemoteMCPServer {
|
|
|
906
1638
|
throw new Error('Remote MCP JWT auth requires SAP_MCP_AUTH_SECRET');
|
|
907
1639
|
}
|
|
908
1640
|
}
|
|
1641
|
+
/**
|
|
1642
|
+
* @name handleStatefulMcpRequest
|
|
1643
|
+
* @description Routes a stateful MCP request to the session transport created by initialize.
|
|
1644
|
+
*/
|
|
1645
|
+
async handleStatefulMcpRequest(req, res, parsedBody) {
|
|
1646
|
+
const transport = await this.resolveStatefulTransport(req, parsedBody);
|
|
1647
|
+
if (!transport) {
|
|
1648
|
+
const providedSessionId = readSingleHeader(req.headers['mcp-session-id']);
|
|
1649
|
+
writeMcpJsonRpcError(res, providedSessionId ? 404 : 400, providedSessionId ? -32011 : -32010, extractJsonRpcId(parsedBody), providedSessionId ? 'mcp_session_not_found' : 'mcp_session_required', {
|
|
1650
|
+
requiredHeader: 'mcp-session-id',
|
|
1651
|
+
...(providedSessionId ? { providedSessionId } : {}),
|
|
1652
|
+
hint: providedSessionId
|
|
1653
|
+
? 'The mcp-session-id must come from this server initialize response. Client-generated UUIDs are not valid MCP sessions.'
|
|
1654
|
+
: 'Call initialize first and reuse the returned mcp-session-id header. Do not generate a client-side UUID.',
|
|
1655
|
+
});
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* @name validateStatefulPaidRequest
|
|
1662
|
+
* @description Fails paid tool calls before x402 verification when the MCP session is missing or unknown.
|
|
1663
|
+
*/
|
|
1664
|
+
validateStatefulPaidRequest(req, parsedBody) {
|
|
1665
|
+
if (isInitializeJsonRpcRequest(parsedBody)) {
|
|
1666
|
+
return undefined;
|
|
1667
|
+
}
|
|
1668
|
+
const sessionId = readSingleHeader(req.headers['mcp-session-id']);
|
|
1669
|
+
if (!sessionId) {
|
|
1670
|
+
return {
|
|
1671
|
+
code: -32010,
|
|
1672
|
+
message: 'mcp_session_required',
|
|
1673
|
+
data: {
|
|
1674
|
+
requiredHeader: 'mcp-session-id',
|
|
1675
|
+
hint: 'Initialize the MCP Streamable HTTP session first, then reuse the returned mcp-session-id for the unpaid payment challenge and the paid retry.',
|
|
1676
|
+
},
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
if (!this.statefulSessions.has(sessionId)) {
|
|
1680
|
+
return {
|
|
1681
|
+
code: -32011,
|
|
1682
|
+
message: 'mcp_session_not_found',
|
|
1683
|
+
data: {
|
|
1684
|
+
requiredHeader: 'mcp-session-id',
|
|
1685
|
+
providedSessionId: sessionId,
|
|
1686
|
+
hint: 'The mcp-session-id must come from this server initialize response. Client-generated UUIDs are not valid MCP sessions.',
|
|
1687
|
+
},
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
return undefined;
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* @name resolveStatefulTransport
|
|
1694
|
+
* @description Returns the transport for an existing session or creates one for an initialize request.
|
|
1695
|
+
*/
|
|
1696
|
+
async resolveStatefulTransport(req, parsedBody) {
|
|
1697
|
+
const sessionId = readSingleHeader(req.headers['mcp-session-id']);
|
|
1698
|
+
if (sessionId) {
|
|
1699
|
+
const session = this.statefulSessions.get(sessionId);
|
|
1700
|
+
if (!session) {
|
|
1701
|
+
return undefined;
|
|
1702
|
+
}
|
|
1703
|
+
session.lastSeenAt = Date.now();
|
|
1704
|
+
return session.transport;
|
|
1705
|
+
}
|
|
1706
|
+
if (req.method === 'POST' && (parsedBody === undefined || isInitializeJsonRpcRequest(parsedBody))) {
|
|
1707
|
+
return await this.createStatefulSessionTransport();
|
|
1708
|
+
}
|
|
1709
|
+
return undefined;
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* @name createStatefulSessionTransport
|
|
1713
|
+
* @description Creates one MCP server and Streamable HTTP transport for a single initialized session.
|
|
1714
|
+
*/
|
|
1715
|
+
async createStatefulSessionTransport() {
|
|
1716
|
+
const server = await createSapMcpServer(this.appConfig);
|
|
1717
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1718
|
+
sessionIdGenerator: () => randomUUID(),
|
|
1719
|
+
onsessioninitialized: sessionId => {
|
|
1720
|
+
this.statefulSessions.set(sessionId, {
|
|
1721
|
+
server,
|
|
1722
|
+
transport,
|
|
1723
|
+
createdAt: Date.now(),
|
|
1724
|
+
lastSeenAt: Date.now(),
|
|
1725
|
+
});
|
|
1726
|
+
logger.info('Stateful MCP session initialized', {
|
|
1727
|
+
sessionId,
|
|
1728
|
+
activeSessions: this.statefulSessions.size,
|
|
1729
|
+
});
|
|
1730
|
+
},
|
|
1731
|
+
});
|
|
1732
|
+
transport.onclose = () => {
|
|
1733
|
+
const sessionId = transport.sessionId;
|
|
1734
|
+
if (sessionId) {
|
|
1735
|
+
this.statefulSessions.delete(sessionId);
|
|
1736
|
+
logger.info('Stateful MCP session closed', {
|
|
1737
|
+
sessionId,
|
|
1738
|
+
activeSessions: this.statefulSessions.size,
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
await server.connect(transport);
|
|
1743
|
+
return transport;
|
|
1744
|
+
}
|
|
909
1745
|
/**
|
|
910
1746
|
* @name applyCors
|
|
911
1747
|
* @description Applies configured CORS headers and handles preflight requests.
|