@gavdi/cap-mcp 1.5.1 → 1.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/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
@@ -294,6 +294,40 @@ extend projection Books with actions {
294
294
  }
295
295
  ```
296
296
 
297
+ #### Required vs Optional Parameters
298
+
299
+ Parameters follow standard CDS nullability rules. A parameter declared `not null` is **required** in the MCP tool schema; a parameter without `not null` is **optional** and may be omitted by the AI agent.
300
+
301
+ ```cds
302
+ @mcp: {
303
+ name : 'search-items',
304
+ description: 'Search for items by keyword with optional filters',
305
+ tool : true
306
+ }
307
+ function searchItems(
308
+ query : String not null, // required — must be provided
309
+ category : String, // optional — may be omitted
310
+ limit : Integer, // optional — may be omitted
311
+ format : String // optional — may be omitted
312
+ ) returns array of String;
313
+ ```
314
+
315
+ The generated JSON Schema will list only `not null` parameters in the `required` array:
316
+
317
+ ```json
318
+ {
319
+ "properties": {
320
+ "query": { "type": "string" },
321
+ "category": { "type": "string" },
322
+ "limit": { "type": "integer" },
323
+ "format": { "type": "string" }
324
+ },
325
+ "required": ["query"]
326
+ }
327
+ ```
328
+
329
+ > **Note:** This behaviour applies to unbound functions and actions. Entity wrapper tools (`query`, `get`, `create`, `update`) derive nullability from the entity element definitions.
330
+
297
331
  #### Tool Elicitation
298
332
 
299
333
  Request user confirmation or input before tool execution using the `elicit` property:
@@ -236,7 +236,8 @@ function parseOperationElements(annotations, model) {
236
236
  }
237
237
  continue;
238
238
  }
239
- parseParam(k, v);
239
+ const optionalSuffix = v.notNull ? undefined : "Optional";
240
+ parseParam(k, v, optionalSuffix);
240
241
  }
241
242
  }
242
243
  return {
package/lib/auth/utils.js CHANGED
@@ -304,111 +304,112 @@ function registerOAuthEndpoints(expressApp, credentials, kind) {
304
304
  // OAuth Dynamic Client Registration discovery endpoint (GET)
305
305
  expressApp.get("/oauth/register", async (req, res) => {
306
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
307
308
  // IAS does not support DCR so we will respond with the pre-configured client_id
308
- if (kind === "ias") {
309
- const enhancedResponse = {
310
- client_id: credentials.clientid, // Add our CAP app's client ID
311
- redirect_uris: req.body.redirect_uris || [
312
- `${baseUrl}/oauth/callback`,
313
- ],
314
- };
315
- logger_1.LOGGER.debug("Provided static client_id during DCR registration process");
316
- res.json(enhancedResponse);
317
- return;
318
- }
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
+ // }
319
318
  // Keep original implementation for XSUAA
320
- try {
321
- // Simple proxy for discovery - no CSRF needed
322
- const response = await fetch(`${credentials.url}/oauth/register`, {
323
- method: "GET",
324
- headers: {
325
- Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
326
- Accept: "application/json",
327
- },
328
- });
329
- const xsuaaData = await response.json();
330
- // Add missing required fields that MCP client expects
331
- const enhancedResponse = {
332
- ...xsuaaData, // Keep all XSUAA fields
333
- client_id: credentials.clientid, // Add our CAP app's client ID
334
- redirect_uris: [`${baseUrl}/oauth/callback`], // Add our callback URL for discovery
335
- };
336
- res.status(response.status).json(enhancedResponse);
337
- }
338
- catch (error) {
339
- logger_1.LOGGER.error("OAuth registration discovery error:", error);
340
- res.status(500).json({
341
- error: "server_error",
342
- error_description: error instanceof Error ? error.message : "Unknown error",
343
- });
344
- }
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
+ // }
345
344
  });
346
345
  // OAuth Dynamic Client Registration endpoint (POST) with CSRF handling
347
346
  expressApp.post("/oauth/register", async (req, res) => {
348
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
349
349
  // IAS does not support DCR so we will respond with the pre-configured client_id
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 || [
354
- `${baseUrl}/oauth/callback`,
355
- ],
356
- };
357
- logger_1.LOGGER.debug("Provided static client_id during DCR registration process");
358
- res.json(enhancedResponse);
359
- return;
360
- }
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
+ // }
361
359
  // Keep original implementation for XSUAA
362
- try {
363
- // Step 1: Fetch CSRF token from XSUAA
364
- const csrfResponse = await fetch(`${credentials.url}/oauth/register`, {
365
- method: "GET",
366
- headers: {
367
- "X-CSRF-Token": "Fetch",
368
- Authorization: `Basic ${Buffer.from(`${credentials.clientid}:${credentials.clientsecret}`).toString("base64")}`,
369
- Accept: "application/json",
370
- },
371
- });
372
- if (!csrfResponse.ok) {
373
- throw new Error(`CSRF fetch failed: ${csrfResponse.status}`);
374
- }
375
- // Step 2: Extract CSRF token and session cookie
376
- const setCookieHeader = csrfResponse.headers.get("set-cookie") || "";
377
- const csrfToken = extractCsrfFromCookie(setCookieHeader);
378
- if (!csrfToken) {
379
- throw new Error("Could not extract CSRF token from XSUAA response");
380
- }
381
- // Step 3: Make actual registration POST with CSRF token
382
- const registrationResponse = await fetch(`${credentials.url}/oauth/register`, {
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
- const xsuaaData = await registrationResponse.json();
394
- // Add missing required fields that MCP client expects
395
- const enhancedResponse = {
396
- ...xsuaaData, // Keep all XSUAA fields
397
- client_id: credentials.clientid, // Add our CAP app's client ID
398
- redirect_uris: req.body.redirect_uris || [
399
- `${baseUrl}/oauth/callback`,
400
- ],
401
- };
402
- logger_1.LOGGER.debug("[AUTH] Register POST response", enhancedResponse);
403
- res.status(registrationResponse.status).json(enhancedResponse);
404
- }
405
- catch (error) {
406
- logger_1.LOGGER.error("OAuth registration error:", error);
407
- res.status(500).json({
408
- error: "server_error",
409
- error_description: error instanceof Error ? error.message : "Unknown error",
410
- });
411
- }
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
+ // }
412
413
  });
413
414
  logger_1.LOGGER.debug("OAuth endpoints registered for XSUAA integration");
414
415
  }
package/lib/mcp/utils.js CHANGED
@@ -17,6 +17,10 @@ const cds = global.cds || require("@sap/cds"); // This is a work around for miss
17
17
  * @returns Zod schema instance for the given type
18
18
  */
19
19
  function determineMcpParameterType(cdsType, key, target) {
20
+ if (cdsType?.endsWith("Optional")) {
21
+ const baseType = cdsType.slice(0, -"Optional".length);
22
+ return determineMcpParameterType(baseType, key, target).optional();
23
+ }
20
24
  switch (cdsType) {
21
25
  case "String":
22
26
  return zod_1.z.string();
@@ -195,14 +199,14 @@ function buildDeepInsertZodType(targetEntityName) {
195
199
  async function handleMcpSessionRequest(req, res, sessions) {
196
200
  const sessionIdHeader = req.headers[constants_1.MCP_SESSION_HEADER];
197
201
  if (!sessionIdHeader || !sessions.has(sessionIdHeader)) {
198
- res.status(400).send("Invalid or missing session ID");
202
+ res.status(404).json({
203
+ jsonrpc: "2.0",
204
+ error: { code: -32001, message: "Session not found" },
205
+ id: null,
206
+ });
199
207
  return;
200
208
  }
201
209
  const session = sessions.get(sessionIdHeader);
202
- if (!session) {
203
- res.status(400).send("Invalid session");
204
- return;
205
- }
206
210
  await session.transport.handleRequest(req, res);
207
211
  }
208
212
  /**
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.1",
3
+ "version": "1.7.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",