@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 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 4 or higher
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("/.well-known/oauth-protected-resource", (req, res) => {
287
- const baseUrl = (0, host_resolver_1.buildPublicBaseUrl)(req);
288
- res.json({
289
- resource: baseUrl,
290
- authorization_servers: [credentials.url],
291
- bearer_methods_supported: ["header"],
292
- resource_documentation: `${baseUrl}/mcp/health`,
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
- const enhancedResponse = {
301
- client_id: credentials.clientid, // Add our CAP app's client ID
302
- redirect_uris: req.body.redirect_uris || [
303
- `${baseUrl}/oauth/callback`,
304
- ],
305
- };
306
- logger_1.LOGGER.debug("Provided static client_id during DCR registration process");
307
- res.json(enhancedResponse);
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
- // Simple proxy for discovery - no CSRF needed
313
- const response = await fetch(`${credentials.url}/oauth/register`, {
314
- method: "GET",
315
- headers: {
316
- Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
317
- Accept: "application/json",
318
- },
319
- });
320
- const xsuaaData = await response.json();
321
- // Add missing required fields that MCP client expects
322
- const enhancedResponse = {
323
- ...xsuaaData, // Keep all XSUAA fields
324
- client_id: credentials.clientid, // Add our CAP app's client ID
325
- redirect_uris: [`${baseUrl}/oauth/callback`], // Add our callback URL for discovery
326
- };
327
- res.status(response.status).json(enhancedResponse);
328
- }
329
- catch (error) {
330
- logger_1.LOGGER.error("OAuth registration discovery error:", error);
331
- res.status(500).json({
332
- error: "server_error",
333
- error_description: error instanceof Error ? error.message : "Unknown error",
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
- const enhancedResponse = {
343
- client_id: credentials.clientid, // Add our CAP app's client ID
344
- redirect_uris: req.body.redirect_uris || [
345
- `${baseUrl}/oauth/callback`,
346
- ],
347
- };
348
- logger_1.LOGGER.debug("Provided static client_id during DCR registration process");
349
- res.json(enhancedResponse);
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
- // Step 1: Fetch CSRF token from XSUAA
355
- const csrfResponse = await fetch(`${credentials.url}/oauth/register`, {
356
- method: "GET",
357
- headers: {
358
- "X-CSRF-Token": "Fetch",
359
- Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
360
- Accept: "application/json",
361
- },
362
- });
363
- if (!csrfResponse.ok) {
364
- throw new Error(`CSRF fetch failed: ${csrfResponse.status}`);
365
- }
366
- // Step 2: Extract CSRF token and session cookie
367
- const setCookieHeader = csrfResponse.headers.get("set-cookie") || "";
368
- const csrfToken = extractCsrfFromCookie(setCookieHeader);
369
- if (!csrfToken) {
370
- throw new Error("Could not extract CSRF token from XSUAA response");
371
- }
372
- // Step 3: Make actual registration POST with CSRF token
373
- const registrationResponse = await fetch(`${credentials.url}/oauth/register`, {
374
- method: "POST",
375
- headers: {
376
- "Content-Type": "application/json",
377
- "X-CSRF-Token": csrfToken,
378
- Cookie: setCookieHeader,
379
- Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
380
- Accept: "application/json",
381
- },
382
- body: JSON.stringify(req.body),
383
- });
384
- const xsuaaData = await registrationResponse.json();
385
- // Add missing required fields that MCP client expects
386
- const enhancedResponse = {
387
- ...xsuaaData, // Keep all XSUAA fields
388
- client_id: credentials.clientid, // Add our CAP app's client ID
389
- redirect_uris: req.body.redirect_uris || [
390
- `${baseUrl}/oauth/callback`,
391
- ],
392
- };
393
- logger_1.LOGGER.debug("[AUTH] Register POST response", enhancedResponse);
394
- res.status(registrationResponse.status).json(enhancedResponse);
395
- }
396
- catch (error) {
397
- logger_1.LOGGER.error("OAuth registration error:", error);
398
- res.status(500).json({
399
- error: "server_error",
400
- error_description: error instanceof Error ? error.message : "Unknown error",
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(400).send("Invalid or missing session ID");
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(400).json({
107
+ return res.status(404).json({
108
108
  jsonrpc: "2.0",
109
109
  error: {
110
- code: -32000,
111
- message: "Bad Request: No valid sessions ID provided",
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(400).json({
142
+ res.status(404).json({
143
143
  jsonrpc: "2.0",
144
144
  error: {
145
- code: -32000,
146
- message: "Bad Request: No valid sessions ID provided",
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.5.0",
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": "^4"
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": "^24.0.3",
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",