@gavdi/cap-mcp 1.6.0 → 1.8.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
@@ -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 {
@@ -358,7 +358,16 @@ function registerQueryTool(resAnno, server, authEnabled) {
358
358
  return (0, utils_2.toolError)("QUERY_FAILED", msg);
359
359
  }
360
360
  };
361
- server.registerTool(toolName, { title: toolName, description: desc, inputSchema }, queryHandler);
361
+ server.registerTool(toolName, {
362
+ title: toolName,
363
+ description: desc,
364
+ inputSchema,
365
+ annotations: {
366
+ readOnlyHint: true,
367
+ destructiveHint: false,
368
+ idempotentHint: true,
369
+ },
370
+ }, queryHandler);
362
371
  }
363
372
  /**
364
373
  * Registers the get-by-keys tool for an entity.
@@ -429,7 +438,16 @@ function registerGetTool(resAnno, server, authEnabled) {
429
438
  return (0, utils_2.toolError)("GET_FAILED", msg);
430
439
  }
431
440
  };
432
- server.registerTool(toolName, { title: toolName, description: desc, inputSchema }, getHandler);
441
+ server.registerTool(toolName, {
442
+ title: toolName,
443
+ description: desc,
444
+ inputSchema,
445
+ annotations: {
446
+ readOnlyHint: true,
447
+ destructiveHint: false,
448
+ idempotentHint: true,
449
+ },
450
+ }, getHandler);
433
451
  }
434
452
  /**
435
453
  * Registers the create tool for an entity.
@@ -528,7 +546,16 @@ function registerCreateTool(resAnno, server, authEnabled) {
528
546
  return (0, utils_2.toolError)(isTimeout ? "TIMEOUT" : "CREATE_FAILED", msg);
529
547
  }
530
548
  };
531
- server.registerTool(toolName, { title: toolName, description: desc, inputSchema }, createHandler);
549
+ server.registerTool(toolName, {
550
+ title: toolName,
551
+ description: desc,
552
+ inputSchema,
553
+ annotations: {
554
+ readOnlyHint: false,
555
+ destructiveHint: false,
556
+ idempotentHint: false,
557
+ },
558
+ }, createHandler);
532
559
  }
533
560
  /**
534
561
  * Registers the update tool for an entity.
@@ -647,7 +674,16 @@ function registerUpdateTool(resAnno, server, authEnabled) {
647
674
  return (0, utils_2.toolError)(isTimeout ? "TIMEOUT" : "UPDATE_FAILED", msg);
648
675
  }
649
676
  };
650
- server.registerTool(toolName, { title: toolName, description: desc, inputSchema }, updateHandler);
677
+ server.registerTool(toolName, {
678
+ title: toolName,
679
+ description: desc,
680
+ inputSchema,
681
+ annotations: {
682
+ readOnlyHint: false,
683
+ destructiveHint: false,
684
+ idempotentHint: true,
685
+ },
686
+ }, updateHandler);
651
687
  }
652
688
  /**
653
689
  * Registers the delete tool for an entity.
@@ -716,7 +752,16 @@ function registerDeleteTool(resAnno, server, authEnabled) {
716
752
  return (0, utils_2.toolError)(isTimeout ? "TIMEOUT" : "DELETE_FAILED", msg);
717
753
  }
718
754
  };
719
- server.registerTool(toolName, { title: toolName, description: desc, inputSchema }, deleteHandler);
755
+ server.registerTool(toolName, {
756
+ title: toolName,
757
+ description: desc,
758
+ inputSchema,
759
+ annotations: {
760
+ readOnlyHint: false,
761
+ destructiveHint: true,
762
+ idempotentHint: true,
763
+ },
764
+ }, deleteHandler);
720
765
  }
721
766
  // Helper: compile structured inputs into a CDS query
722
767
  // The function translates the validated MCP input into CQN safely,
package/lib/mcp/tools.js CHANGED
@@ -25,6 +25,26 @@ function assignToolToServer(model, server, authEnabled) {
25
25
  }
26
26
  assignUnboundOperation(parameters, model, server, authEnabled);
27
27
  }
28
+ /**
29
+ * Derives MCP tool behaviour hints from a CDS operation kind.
30
+ *
31
+ * CDS `function`s are non-side-effecting reads, while `action`s are
32
+ * side-effecting writes. These map onto the MCP `ToolAnnotations` hints so
33
+ * clients can advertise read-only/destructive intent in `tools/list`.
34
+ *
35
+ * Any non-`function` kind (including an undefined kind) is treated as an
36
+ * action to stay on the safe, write-capable side.
37
+ * @param model - The tool annotation carrying the parsed operation kind
38
+ * @returns Tool annotation hints describing the operation's behaviour
39
+ */
40
+ function buildOperationAnnotations(model) {
41
+ const readOnly = model.operationKind === "function";
42
+ return {
43
+ readOnlyHint: readOnly,
44
+ destructiveHint: !readOnly,
45
+ idempotentHint: readOnly,
46
+ };
47
+ }
28
48
  /**
29
49
  * Registers a bound operation that operates on a specific entity instance
30
50
  * Requires entity key parameters in addition to operation parameters
@@ -48,6 +68,7 @@ function assignBoundOperation(params, model, server, authEnabled) {
48
68
  title: model.name,
49
69
  description: model.description,
50
70
  inputSchema: inputSchema,
71
+ annotations: buildOperationAnnotations(model),
51
72
  }, async (args) => {
52
73
  // Resolve from current CAP context; prefer global to align with Jest mocks
53
74
  const cdsMod = global.cds || cds;
@@ -104,6 +125,7 @@ function assignUnboundOperation(params, model, server, authEnabled) {
104
125
  title: model.name,
105
126
  description: model.description,
106
127
  inputSchema: inputSchema,
128
+ annotations: buildOperationAnnotations(model),
107
129
  }, async (args) => {
108
130
  // Resolve from current CAP context; prefer global to align with Jest mocks
109
131
  const cdsMod = global.cds || cds;
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();
package/lib/mcp.js CHANGED
@@ -57,6 +57,12 @@ class McpPlugin {
57
57
  },
58
58
  },
59
59
  }));
60
+ // Apply body limit
61
+ const limit = cds.env.mcp?.body_parser?.limit ??
62
+ cds.env.server?.body_parser?.limit ??
63
+ "100kb"; // preserve current default if no config
64
+ this.expressApp.use("/mcp", express_1.default.json({ limit }));
65
+ // Handle auth configuration
60
66
  if (this.config.auth === "inherit") {
61
67
  (0, utils_2.registerAuthMiddleware)(this.expressApp);
62
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gavdi/cap-mcp",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "MCP Plugin for CAP",
5
5
  "keywords": [
6
6
  "MCP",
@@ -43,24 +43,24 @@
43
43
  "docs:links": "markdown-link-check docs/**/*.md"
44
44
  },
45
45
  "peerDependencies": {
46
- "@sap/cds": ">=9",
46
+ "@sap/cds": ">=10",
47
47
  "express": "^5"
48
48
  },
49
49
  "dependencies": {
50
50
  "@modelcontextprotocol/sdk": "^1.23.0",
51
- "@sap/xssec": "^4.9.1",
51
+ "@sap/xssec": "^4.13.3",
52
52
  "cors": "^2.8.5",
53
53
  "helmet": "^8.1.0",
54
54
  "zod": "^3.25.67",
55
55
  "zod-to-json-schema": "^3.24.5"
56
56
  },
57
57
  "devDependencies": {
58
- "@cap-js/cds-types": "^0.13.0",
59
- "@release-it/conventional-changelog": "^10.0.1",
58
+ "@cap-js/cds-types": "^0.18.0",
59
+ "@release-it/conventional-changelog": "^11.0.1",
60
60
  "@types/cors": "^2.8.19",
61
61
  "@types/express": "^5.0.3",
62
62
  "@types/jest": "^30.0.0",
63
- "@types/node": "^25.3.0",
63
+ "@types/node": "^26.1.1",
64
64
  "@types/sinon": "^17.0.4",
65
65
  "@types/supertest": "^6.0.2",
66
66
  "docsify-cli": "^4.4.4",
@@ -70,7 +70,7 @@
70
70
  "lint-staged": "^16.1.2",
71
71
  "markdown-link-check": "^3.14.1",
72
72
  "prettier": "^3.5.3",
73
- "release-it": "^19.0.4",
73
+ "release-it": "^20.2.1",
74
74
  "sinon": "^21.0.0",
75
75
  "supertest": "^7.0.0",
76
76
  "ts-jest": "^29.4.0",