@gavdi/cap-mcp 1.5.0 → 1.6.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/README.md +1 -1
- package/lib/auth/utils.js +116 -106
- package/lib/mcp/utils.js +5 -5
- package/lib/mcp.js +9 -9
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ By integrating MCP with your CAP applications, you unlock:
|
|
|
29
29
|
|
|
30
30
|
- **Node.js**: Version 18 or higher
|
|
31
31
|
- **SAP CAP**: Version 9 or higher
|
|
32
|
-
- **Express**: Version
|
|
32
|
+
- **Express**: Version 5 or higher
|
|
33
33
|
- **TypeScript**: Optional but recommended
|
|
34
34
|
|
|
35
35
|
### Step 1: Install the Plugin
|
package/lib/auth/utils.js
CHANGED
|
@@ -282,124 +282,134 @@ function registerOAuthEndpoints(expressApp, credentials, kind) {
|
|
|
282
282
|
registration_endpoint_auth_methods_supported: ["client_secret_basic"],
|
|
283
283
|
});
|
|
284
284
|
});
|
|
285
|
+
// BUG: This element has been commented out as a part of a hotfix for authorization flows.
|
|
286
|
+
// It should not be included again until further investigation has been done, but a patch will have to be released to remedy this.
|
|
287
|
+
// This is likely related to the fact that most MCP clients do not include application/json as their preferred response time when authenticating,
|
|
288
|
+
// causing issues when targeting SAP's XSUAA service, that will default to HTML.
|
|
289
|
+
//
|
|
285
290
|
// RFC 9728: OAuth 2.0 Protected Resource Metadata endpoint
|
|
286
|
-
expressApp.get(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
291
|
+
// expressApp.get(
|
|
292
|
+
// "/.well-known/oauth-protected-resource",
|
|
293
|
+
// (req: Request, res: Response): void => {
|
|
294
|
+
// const baseUrl = buildPublicBaseUrl(req);
|
|
295
|
+
//
|
|
296
|
+
// res.json({
|
|
297
|
+
// resource: baseUrl,
|
|
298
|
+
// authorization_servers: [credentials.url],
|
|
299
|
+
// bearer_methods_supported: ["header"],
|
|
300
|
+
// resource_documentation: `${baseUrl}/mcp/health`,
|
|
301
|
+
// });
|
|
302
|
+
// },
|
|
303
|
+
// );
|
|
295
304
|
// OAuth Dynamic Client Registration discovery endpoint (GET)
|
|
296
305
|
expressApp.get("/oauth/register", async (req, res) => {
|
|
297
306
|
const baseUrl = (0, host_resolver_1.buildPublicBaseUrl)(req);
|
|
307
|
+
// XSUAA does not support DCR so we will respond with the pre-configured client_id
|
|
298
308
|
// IAS does not support DCR so we will respond with the pre-configured client_id
|
|
299
|
-
if (kind === "ias") {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
309
|
+
// if (kind === "ias") {
|
|
310
|
+
const enhancedResponse = {
|
|
311
|
+
client_id: credentials.clientid, // Add our CAP app's client ID
|
|
312
|
+
redirect_uris: req.body.redirect_uris || [`${baseUrl}/oauth/callback`],
|
|
313
|
+
};
|
|
314
|
+
logger_1.LOGGER.debug("Provided static client_id during DCR registration process");
|
|
315
|
+
res.json(enhancedResponse);
|
|
316
|
+
return;
|
|
317
|
+
// }
|
|
310
318
|
// Keep original implementation for XSUAA
|
|
311
|
-
try {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
319
|
+
// try {
|
|
320
|
+
// // Simple proxy for discovery - no CSRF needed
|
|
321
|
+
// const response = await fetch(`${credentials.url}/oauth/register`, {
|
|
322
|
+
// method: "GET",
|
|
323
|
+
// headers: {
|
|
324
|
+
// Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
|
|
325
|
+
// Accept: "application/json",
|
|
326
|
+
// },
|
|
327
|
+
// });
|
|
328
|
+
// const xsuaaData = await response.json();
|
|
329
|
+
// // Add missing required fields that MCP client expects
|
|
330
|
+
// const enhancedResponse = {
|
|
331
|
+
// ...xsuaaData, // Keep all XSUAA fields
|
|
332
|
+
// client_id: credentials.clientid, // Add our CAP app's client ID
|
|
333
|
+
// redirect_uris: [`${baseUrl}/oauth/callback`], // Add our callback URL for discovery
|
|
334
|
+
// };
|
|
335
|
+
// res.status(response.status).json(enhancedResponse);
|
|
336
|
+
// } catch (error) {
|
|
337
|
+
// LOGGER.error("OAuth registration discovery error:", error);
|
|
338
|
+
// res.status(500).json({
|
|
339
|
+
// error: "server_error",
|
|
340
|
+
// error_description:
|
|
341
|
+
// error instanceof Error ? error.message : "Unknown error",
|
|
342
|
+
// });
|
|
343
|
+
// }
|
|
336
344
|
});
|
|
337
345
|
// OAuth Dynamic Client Registration endpoint (POST) with CSRF handling
|
|
338
346
|
expressApp.post("/oauth/register", async (req, res) => {
|
|
339
347
|
const baseUrl = (0, host_resolver_1.buildPublicBaseUrl)(req);
|
|
348
|
+
// XSUAA does not support DCR so we will respond with the pre-configured client_id
|
|
340
349
|
// IAS does not support DCR so we will respond with the pre-configured client_id
|
|
341
|
-
if (kind === "ias") {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
350
|
+
// if (kind === "ias") {
|
|
351
|
+
const enhancedResponse = {
|
|
352
|
+
client_id: credentials.clientid, // Add our CAP app's client ID
|
|
353
|
+
redirect_uris: req.body.redirect_uris || [`${baseUrl}/oauth/callback`],
|
|
354
|
+
};
|
|
355
|
+
logger_1.LOGGER.debug("Provided static client_id during DCR registration process");
|
|
356
|
+
res.json(enhancedResponse);
|
|
357
|
+
return;
|
|
358
|
+
// }
|
|
352
359
|
// Keep original implementation for XSUAA
|
|
353
|
-
try {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
360
|
+
// try {
|
|
361
|
+
// Step 1: Fetch CSRF token from XSUAA
|
|
362
|
+
// const csrfResponse = await fetch(`${credentials.url}/oauth/register`, {
|
|
363
|
+
// method: "GET",
|
|
364
|
+
// headers: {
|
|
365
|
+
// "X-CSRF-Token": "Fetch",
|
|
366
|
+
// Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
|
|
367
|
+
// Accept: "application/json",
|
|
368
|
+
// },
|
|
369
|
+
// });
|
|
370
|
+
// if (!csrfResponse.ok) {
|
|
371
|
+
// throw new Error(`CSRF fetch failed: ${csrfResponse.status}`);
|
|
372
|
+
// }
|
|
373
|
+
// Step 2: Extract CSRF token and session cookie
|
|
374
|
+
// const setCookieHeader = csrfResponse.headers.get("set-cookie") || "";
|
|
375
|
+
// const csrfToken = extractCsrfFromCookie(setCookieHeader);
|
|
376
|
+
// if (!csrfToken) {
|
|
377
|
+
// throw new Error("Could not extract CSRF token from XSUAA response");
|
|
378
|
+
// }
|
|
379
|
+
// Step 3: Make actual registration POST with CSRF token
|
|
380
|
+
// const registrationResponse = await fetch(
|
|
381
|
+
// `${credentials.url}/oauth/register`,
|
|
382
|
+
// {
|
|
383
|
+
// method: "POST",
|
|
384
|
+
// headers: {
|
|
385
|
+
// "Content-Type": "application/json",
|
|
386
|
+
// "X-CSRF-Token": csrfToken,
|
|
387
|
+
// Cookie: setCookieHeader,
|
|
388
|
+
// Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
|
|
389
|
+
// Accept: "application/json",
|
|
390
|
+
// },
|
|
391
|
+
// body: JSON.stringify(req.body),
|
|
392
|
+
// },
|
|
393
|
+
// );
|
|
394
|
+
// const xsuaaData = await registrationResponse.json();
|
|
395
|
+
// Add missing required fields that MCP client expects
|
|
396
|
+
// const enhancedResponse = {
|
|
397
|
+
// ...xsuaaData, // Keep all XSUAA fields
|
|
398
|
+
// client_id: credentials.clientid, // Add our CAP app's client ID
|
|
399
|
+
// redirect_uris: req.body.redirect_uris || [
|
|
400
|
+
// `${baseUrl}/oauth/callback`,
|
|
401
|
+
// ],
|
|
402
|
+
// };
|
|
403
|
+
// LOGGER.debug("[AUTH] Register POST response", enhancedResponse);
|
|
404
|
+
// res.status(registrationResponse.status).json(enhancedResponse);
|
|
405
|
+
// } catch (error) {
|
|
406
|
+
// LOGGER.error("OAuth registration error:", error);
|
|
407
|
+
// res.status(500).json({
|
|
408
|
+
// error: "server_error",
|
|
409
|
+
// error_description:
|
|
410
|
+
// error instanceof Error ? error.message : "Unknown error",
|
|
411
|
+
// });
|
|
412
|
+
// }
|
|
403
413
|
});
|
|
404
414
|
logger_1.LOGGER.debug("OAuth endpoints registered for XSUAA integration");
|
|
405
415
|
}
|
package/lib/mcp/utils.js
CHANGED
|
@@ -195,14 +195,14 @@ function buildDeepInsertZodType(targetEntityName) {
|
|
|
195
195
|
async function handleMcpSessionRequest(req, res, sessions) {
|
|
196
196
|
const sessionIdHeader = req.headers[constants_1.MCP_SESSION_HEADER];
|
|
197
197
|
if (!sessionIdHeader || !sessions.has(sessionIdHeader)) {
|
|
198
|
-
res.status(
|
|
198
|
+
res.status(404).json({
|
|
199
|
+
jsonrpc: "2.0",
|
|
200
|
+
error: { code: -32001, message: "Session not found" },
|
|
201
|
+
id: null,
|
|
202
|
+
});
|
|
199
203
|
return;
|
|
200
204
|
}
|
|
201
205
|
const session = sessions.get(sessionIdHeader);
|
|
202
|
-
if (!session) {
|
|
203
|
-
res.status(400).send("Invalid session");
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
206
|
await session.transport.handleRequest(req, res);
|
|
207
207
|
}
|
|
208
208
|
/**
|
package/lib/mcp.js
CHANGED
|
@@ -104,13 +104,13 @@ class McpPlugin {
|
|
|
104
104
|
const sessionIdHeader = req.headers[constants_1.MCP_SESSION_HEADER];
|
|
105
105
|
const sessions = this.sessionManager.getSessions();
|
|
106
106
|
if (!sessionIdHeader || !sessions.has(sessionIdHeader)) {
|
|
107
|
-
return res.status(
|
|
107
|
+
return res.status(404).json({
|
|
108
108
|
jsonrpc: "2.0",
|
|
109
109
|
error: {
|
|
110
|
-
code: -
|
|
111
|
-
message: "
|
|
112
|
-
id: null,
|
|
110
|
+
code: -32001,
|
|
111
|
+
message: "Session not found",
|
|
113
112
|
},
|
|
113
|
+
id: null,
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
const session = sessions.get(sessionIdHeader);
|
|
@@ -139,13 +139,13 @@ class McpPlugin {
|
|
|
139
139
|
: this.sessionManager.getSession(sessionIdHeader);
|
|
140
140
|
if (!session) {
|
|
141
141
|
logger_1.LOGGER.error("Invalid session ID", sessionIdHeader);
|
|
142
|
-
res.status(
|
|
142
|
+
res.status(404).json({
|
|
143
143
|
jsonrpc: "2.0",
|
|
144
144
|
error: {
|
|
145
|
-
code: -
|
|
146
|
-
message: "
|
|
147
|
-
id: null,
|
|
145
|
+
code: -32001,
|
|
146
|
+
message: "Session not found",
|
|
148
147
|
},
|
|
148
|
+
id: null,
|
|
149
149
|
});
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
@@ -163,8 +163,8 @@ class McpPlugin {
|
|
|
163
163
|
error: {
|
|
164
164
|
code: -32603,
|
|
165
165
|
message: "Internal Error: Transport failed",
|
|
166
|
-
id: null,
|
|
167
166
|
},
|
|
167
|
+
id: null,
|
|
168
168
|
});
|
|
169
169
|
return;
|
|
170
170
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gavdi/cap-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "MCP Plugin for CAP",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"MCP",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@sap/cds": ">=9",
|
|
47
|
-
"express": "^
|
|
47
|
+
"express": "^5"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.23.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@types/cors": "^2.8.19",
|
|
61
61
|
"@types/express": "^5.0.3",
|
|
62
62
|
"@types/jest": "^30.0.0",
|
|
63
|
-
"@types/node": "^
|
|
63
|
+
"@types/node": "^25.3.0",
|
|
64
64
|
"@types/sinon": "^17.0.4",
|
|
65
65
|
"@types/supertest": "^6.0.2",
|
|
66
66
|
"docsify-cli": "^4.4.4",
|