@gavdi/cap-mcp 0.9.9 → 0.10.1

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
@@ -120,6 +120,7 @@ This plugin transforms your annotated CAP services into a fully functional MCP s
120
120
  - **🔧 Tools**: Convert CAP functions and actions into executable MCP tools
121
121
  - **🧩 Entity Wrappers (optional)**: Expose CAP entities as tools (`query`, `get`, and optionally `create`, `update`) for LLM tool use while keeping resources intact
122
122
  - **💡 Prompts**: Define reusable prompt templates for AI interactions
123
+ - **⚡ Elicitation**: Request user confirmation or input parameters before tool execution
123
124
  - **🔄 Auto-generation**: Automatically creates MCP server endpoints based on annotations
124
125
  - **⚙️ Flexible Configuration**: Support for custom parameter sets and descriptions
125
126
 
@@ -231,6 +232,52 @@ extend projection Books with actions {
231
232
  }
232
233
  ```
233
234
 
235
+ #### Tool Elicitation
236
+
237
+ Request user confirmation or input before tool execution using the `elicit` property:
238
+
239
+ ```cds
240
+ // Request user confirmation before execution
241
+ @mcp: {
242
+ name : 'book-recommendation',
243
+ description: 'Get a random book recommendation',
244
+ tool : true,
245
+ elicit : ['confirm']
246
+ }
247
+ function getBookRecommendation() returns String;
248
+
249
+ // Request user input for parameters
250
+ @mcp: {
251
+ name : 'get-author',
252
+ description: 'Gets the desired author',
253
+ tool : true,
254
+ elicit : ['input']
255
+ }
256
+ function getAuthor(id: String) returns String;
257
+
258
+ // Request both input and confirmation
259
+ @mcp: {
260
+ name : 'books-by-author',
261
+ description: 'Gets a list of books made by the author',
262
+ tool : true,
263
+ elicit : ['input', 'confirm']
264
+ }
265
+ function getBooksByAuthor(authorName: String) returns array of String;
266
+ ```
267
+
268
+ > NOTE: Elicitation is only available for direct tools at this moment. Wrapped entities are not covered by this.
269
+
270
+ **Elicit Types:**
271
+ - **`confirm`**: Requests user confirmation before executing the tool with a yes/no prompt
272
+ - **`input`**: Prompts the user to provide values for the tool's parameters
273
+ - **Combined**: Use both `['input', 'confirm']` to first collect parameters, then ask for confirmation
274
+
275
+ **User Experience:**
276
+ - **Confirmation**: "Please confirm that you want to perform action 'Get a random book recommendation'"
277
+ - **Input**: "Please fill out the required parameters" with a form for each parameter
278
+ - **User Actions**: Accept, decline, or cancel the elicitation request
279
+ - **Early Exit**: Tools return appropriate messages if declined or cancelled
280
+
234
281
  ### Prompt Templates
235
282
 
236
283
  Define reusable AI prompt templates:
@@ -27,6 +27,8 @@ exports.MCP_ANNOTATION_PROPS = {
27
27
  MCP_PROMPT: "@mcp.prompts",
28
28
  /** Wrapper configuration for exposing entities as tools */
29
29
  MCP_WRAP: "@mcp.wrap",
30
+ /** Elicited user input annotation for tools in CAP services */
31
+ MCP_ELICIT: "@mcp.elicit",
30
32
  };
31
33
  /**
32
34
  * Set of annotations used for CDS auth annotations
@@ -26,6 +26,13 @@ function parseDefinitions(model) {
26
26
  if (!parsedAnnotations || !(0, utils_1.containsRequiredAnnotations)(parsedAnnotations)) {
27
27
  continue; // This check must occur here, since we do want the bound operations even if the parent is not annotated
28
28
  }
29
+ // Set the target in annotations for error reporting
30
+ if (parsedAnnotations) {
31
+ parsedAnnotations.target = key;
32
+ }
33
+ if (!(0, utils_1.containsRequiredElicitedParams)(parsedAnnotations)) {
34
+ continue; // Really doesn't do anything as the method will throw if the implementation is invalid
35
+ }
29
36
  const verifiedAnnotations = parsedAnnotations;
30
37
  switch (def.kind) {
31
38
  case "entity":
@@ -97,6 +104,9 @@ function parseAnnotations(definition) {
97
104
  // Wrapper container to expose resources as tools
98
105
  annotations.wrap = v;
99
106
  continue;
107
+ case constants_1.MCP_ANNOTATION_PROPS.MCP_ELICIT:
108
+ annotations.elicit = v;
109
+ continue;
100
110
  case constants_1.CDS_AUTH_ANNOTATIONS.REQUIRES:
101
111
  annotations.requires = v;
102
112
  continue;
@@ -139,7 +149,7 @@ function constructToolAnnotation(serviceName, target, annotations, entityKey, ke
139
149
  return undefined;
140
150
  const { parameters, operationKind } = (0, utils_1.parseOperationElements)(annotations);
141
151
  const restrictions = (0, utils_1.parseCdsRestrictions)(annotations.restrict, annotations.requires);
142
- return new structures_1.McpToolAnnotation(annotations.name, annotations.description, target, serviceName, parameters, entityKey, operationKind, keyParams, restrictions);
152
+ return new structures_1.McpToolAnnotation(annotations.name, annotations.description, target, serviceName, parameters, entityKey, operationKind, keyParams, restrictions, annotations.elicit);
143
153
  }
144
154
  /**
145
155
  * Constructs a prompt annotation from parsed annotation data
@@ -172,7 +182,13 @@ function parseBoundOperations(serviceName, entityKey, definition, resultRef) {
172
182
  if (v.kind !== "function" && v.kind !== "action")
173
183
  continue;
174
184
  const parsedAnnotations = parseAnnotations(v);
175
- if (!parsedAnnotations || !(0, utils_1.containsRequiredAnnotations)(parsedAnnotations)) {
185
+ // Set the target in annotations for error reporting
186
+ if (parsedAnnotations) {
187
+ parsedAnnotations.target = k;
188
+ }
189
+ if (!parsedAnnotations ||
190
+ !(0, utils_1.containsRequiredAnnotations)(parsedAnnotations) ||
191
+ !(0, utils_1.containsRequiredElicitedParams)(parsedAnnotations)) {
176
192
  continue;
177
193
  }
178
194
  const verifiedAnnotations = parsedAnnotations;
@@ -142,6 +142,8 @@ class McpToolAnnotation extends McpAnnotation {
142
142
  _operationKind;
143
143
  /** Map of key field names to their types for bound operations */
144
144
  _keyTypeMap;
145
+ /** Elicited user input object */
146
+ _elicits;
145
147
  /**
146
148
  * Creates a new MCP tool annotation
147
149
  * @param name - Unique identifier for this tool
@@ -153,13 +155,15 @@ class McpToolAnnotation extends McpAnnotation {
153
155
  * @param operationKind - Optional operation type ('function' or 'action')
154
156
  * @param keyTypeMap - Optional map of key fields to types for bound operations
155
157
  * @param restrictions - Optional restrictions based on CDS roles
158
+ * @param elicits - Optional elicited input requirement
156
159
  */
157
- constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap, restrictions) {
160
+ constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap, restrictions, elicits) {
158
161
  super(name, description, operation, serviceName, restrictions ?? []);
159
162
  this._parameters = parameters;
160
163
  this._entityKey = entityKey;
161
164
  this._operationKind = operationKind;
162
165
  this._keyTypeMap = keyTypeMap;
166
+ this._elicits = elicits;
163
167
  }
164
168
  /**
165
169
  * Gets the map of function parameters to their CDS types
@@ -189,6 +193,13 @@ class McpToolAnnotation extends McpAnnotation {
189
193
  get keyTypeMap() {
190
194
  return this._keyTypeMap;
191
195
  }
196
+ /**
197
+ * Gets the elicited user input if any is required for the tool
198
+ * @returns Elicited user input object
199
+ */
200
+ get elicits() {
201
+ return this._elicits;
202
+ }
192
203
  }
193
204
  exports.McpToolAnnotation = McpToolAnnotation;
194
205
  /**
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.splitDefinitionName = splitDefinitionName;
4
4
  exports.containsMcpAnnotation = containsMcpAnnotation;
5
5
  exports.containsRequiredAnnotations = containsRequiredAnnotations;
6
+ exports.containsRequiredElicitedParams = containsRequiredElicitedParams;
6
7
  exports.isValidResourceAnnotation = isValidResourceAnnotation;
7
8
  exports.isValidToolAnnotation = isValidToolAnnotation;
8
9
  exports.isValidPromptsAnnotation = isValidPromptsAnnotation;
@@ -55,6 +56,21 @@ function containsRequiredAnnotations(annotations) {
55
56
  }
56
57
  return true;
57
58
  }
59
+ /**
60
+ * Validates that the required params for MCP elicited user input annotations are valid
61
+ * @param annotations - The annotation structure to validate
62
+ * @returns True if valid, throw error if invalid
63
+ * @throws Error if required annotations are missing
64
+ */
65
+ function containsRequiredElicitedParams(annotations) {
66
+ if (!annotations.elicit)
67
+ return true;
68
+ const param = annotations.elicit;
69
+ if (!param || param?.length <= 0) {
70
+ throw new Error(`Invalid annotation '${annotations.target}' - Incomplete elicited user input`);
71
+ }
72
+ return true;
73
+ }
58
74
  /**
59
75
  * Validates a resource annotation structure
60
76
  * @param annotations - The annotation structure to validate
@@ -181,6 +197,8 @@ function parseOperationElements(annotations) {
181
197
  */
182
198
  function parseEntityKeys(definition) {
183
199
  const result = new Map();
200
+ if (!definition?.elements)
201
+ return result; // If there is no defined elements, we exit early
184
202
  for (const [k, v] of Object.entries(definition.elements)) {
185
203
  if (!v.key)
186
204
  continue;
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isElicitInput = isElicitInput;
4
+ exports.constructElicitationFunctions = constructElicitationFunctions;
5
+ exports.handleElicitationRequests = handleElicitationRequests;
6
+ const zod_1 = require("zod");
7
+ /**
8
+ * Message displayed to users when requesting input parameters
9
+ */
10
+ const INPUT_MSG = "Please fill out the required parameters";
11
+ /**
12
+ * Checks if the elicited input array contains an 'input' requirement
13
+ * @param elicits - Array of elicit types or undefined
14
+ * @returns True if 'input' elicitation is required, false otherwise
15
+ */
16
+ function isElicitInput(elicits) {
17
+ return elicits ? elicits.includes("input") : false;
18
+ }
19
+ /**
20
+ * Constructs elicitation request parameters based on the tool annotation's elicit requirements
21
+ * @param model - MCP tool annotation containing elicit configuration
22
+ * @param params - Parameter definitions for the tool
23
+ * @returns Array of elicit request parameters for MCP server
24
+ * @throws Error if invalid elicitation type is encountered
25
+ */
26
+ function constructElicitationFunctions(model, params) {
27
+ const result = [];
28
+ for (const el of model.elicits ?? []) {
29
+ switch (el) {
30
+ case "input":
31
+ result.push(constructElicitInput(params));
32
+ continue;
33
+ case "confirm":
34
+ result.push(contructElicitConfirm(model));
35
+ continue;
36
+ default:
37
+ throw new Error("Invalid elicitation type");
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+ /**
43
+ * Processes multiple elicitation requests sequentially and handles user responses
44
+ * @param requests - Array of elicit request parameters or undefined
45
+ * @param server - MCP server instance for making elicit calls
46
+ * @returns Promise resolving to elicitation response with early exit or data
47
+ */
48
+ async function handleElicitationRequests(requests, server) {
49
+ if (!requests || requests.length <= 0) {
50
+ return { earlyResponse: undefined };
51
+ }
52
+ let data = undefined;
53
+ for (const req of requests) {
54
+ const res = await server.server.elicitInput(req);
55
+ const earlyResponse = handleElicitResponse(res);
56
+ if (earlyResponse) {
57
+ return { earlyResponse };
58
+ }
59
+ if (req.message === INPUT_MSG) {
60
+ data = res.content;
61
+ }
62
+ }
63
+ return {
64
+ earlyResponse: undefined,
65
+ data,
66
+ };
67
+ }
68
+ /**
69
+ * Converts elicit response action into appropriate MCP result
70
+ * @param elicitResponse - Result from MCP server elicit input call
71
+ * @returns MCP result for decline/cancel actions, undefined for accept
72
+ * @throws Error if invalid response action is received
73
+ */
74
+ function handleElicitResponse(elicitResponse) {
75
+ switch (elicitResponse.action) {
76
+ case "accept":
77
+ return undefined;
78
+ case "decline":
79
+ return {
80
+ content: [
81
+ {
82
+ type: "text",
83
+ text: "Action was declined.",
84
+ },
85
+ ],
86
+ };
87
+ case "cancel":
88
+ return {
89
+ content: [
90
+ {
91
+ type: "text",
92
+ text: "Action was cancelled",
93
+ },
94
+ ],
95
+ };
96
+ default:
97
+ throw new Error("Invalid elicit response received");
98
+ }
99
+ }
100
+ /**
101
+ * Determines the schema type for elicit input based on Zod parameter type
102
+ * @param param - Zod schema parameter to analyze
103
+ * @returns Corresponding elicit schema type string
104
+ * @throws Error if parameter type is not supported for elicitation
105
+ */
106
+ function determineSchemaType(param) {
107
+ if (param instanceof zod_1.z.ZodBoolean) {
108
+ return "boolean";
109
+ }
110
+ else if (param instanceof zod_1.z.ZodString) {
111
+ return "string";
112
+ }
113
+ else if (param instanceof zod_1.z.ZodNumber) {
114
+ return "number";
115
+ }
116
+ throw new Error("Unsupported elicitation input type");
117
+ }
118
+ /**
119
+ * Constructs confirmation elicit request for tool execution
120
+ * @param model - MCP annotation model containing tool description
121
+ * @returns Elicit request parameters for user confirmation
122
+ */
123
+ function contructElicitConfirm(model) {
124
+ return {
125
+ message: `Please confirm that you want to perform action '${model.description}'`,
126
+ requestedSchema: {
127
+ type: "object",
128
+ properties: {
129
+ confirm: {
130
+ type: "boolean",
131
+ title: "Confirmation",
132
+ description: "Please confirm the action",
133
+ },
134
+ },
135
+ required: ["confirm"],
136
+ },
137
+ };
138
+ }
139
+ /**
140
+ * Constructs input elicit request for tool parameters
141
+ * @param params - Tool parameters definition with Zod schemas
142
+ * @returns Elicit request parameters for user input collection
143
+ */
144
+ function constructElicitInput(params) {
145
+ const elicitSpec = {
146
+ message: INPUT_MSG,
147
+ requestedSchema: {
148
+ type: "object",
149
+ properties: {},
150
+ required: [],
151
+ },
152
+ };
153
+ for (const [key, zodType] of Object.entries(params)) {
154
+ elicitSpec.requestedSchema.required?.push(key);
155
+ elicitSpec.requestedSchema.properties[key] = {
156
+ type: determineSchemaType(zodType),
157
+ title: key,
158
+ description: key,
159
+ };
160
+ }
161
+ return elicitSpec;
162
+ }
package/lib/mcp/tools.js CHANGED
@@ -6,6 +6,7 @@ const logger_1 = require("../logger");
6
6
  const constants_1 = require("./constants");
7
7
  const zod_1 = require("zod");
8
8
  const utils_2 = require("../auth/utils");
9
+ const elicited_input_1 = require("./elicited-input");
9
10
  /* @ts-ignore */
10
11
  const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
11
12
  /**
@@ -37,7 +38,12 @@ function assignBoundOperation(params, model, server, authEnabled) {
37
38
  throw new Error("Bound operation cannot be assigned to tool list, missing keys");
38
39
  }
39
40
  const keys = buildToolParameters(model.keyTypeMap);
40
- const inputSchema = buildZodSchema({ ...keys, ...params });
41
+ const useElicitInput = (0, elicited_input_1.isElicitInput)(model.elicits);
42
+ const inputSchema = buildZodSchema({
43
+ ...keys,
44
+ ...(useElicitInput ? {} : params),
45
+ });
46
+ const elicitationRequests = (0, elicited_input_1.constructElicitationFunctions)(model, params);
41
47
  server.registerTool(model.name, {
42
48
  title: model.name,
43
49
  description: model.description,
@@ -69,11 +75,15 @@ function assignBoundOperation(params, model, server, authEnabled) {
69
75
  continue;
70
76
  operationInput[k] = v;
71
77
  }
78
+ const elicitationResult = await (0, elicited_input_1.handleElicitationRequests)(elicitationRequests, server);
79
+ if (elicitationResult?.earlyResponse) {
80
+ return elicitationResult.earlyResponse;
81
+ }
72
82
  const accessRights = (0, utils_2.getAccessRights)(authEnabled);
73
83
  const response = await service.tx({ user: accessRights }).send({
74
84
  event: model.target,
75
85
  entity: model.entityKey,
76
- data: operationInput,
86
+ data: elicitationResult?.data ?? operationInput,
77
87
  params: [operationKeys],
78
88
  });
79
89
  return (0, utils_1.asMcpResult)(response);
@@ -87,7 +97,9 @@ function assignBoundOperation(params, model, server, authEnabled) {
87
97
  * @param server - MCP server instance to register with
88
98
  */
89
99
  function assignUnboundOperation(params, model, server, authEnabled) {
90
- const inputSchema = buildZodSchema(params);
100
+ const useElicitInput = (0, elicited_input_1.isElicitInput)(model.elicits);
101
+ const inputSchema = buildZodSchema(useElicitInput ? {} : params);
102
+ const elicitationRequests = (0, elicited_input_1.constructElicitationFunctions)(model, params);
91
103
  server.registerTool(model.name, {
92
104
  title: model.name,
93
105
  description: model.description,
@@ -109,10 +121,14 @@ function assignUnboundOperation(params, model, server, authEnabled) {
109
121
  ],
110
122
  };
111
123
  }
124
+ const elicitationResult = await (0, elicited_input_1.handleElicitationRequests)(elicitationRequests, server);
125
+ if (elicitationResult?.earlyResponse) {
126
+ return elicitationResult.earlyResponse;
127
+ }
112
128
  const accessRights = (0, utils_2.getAccessRights)(authEnabled);
113
129
  const response = await service
114
130
  .tx({ user: accessRights })
115
- .send(model.target, args);
131
+ .send(model.target, elicitationResult?.data ?? args);
116
132
  return (0, utils_1.asMcpResult)(response);
117
133
  });
118
134
  }
@@ -130,27 +146,6 @@ function buildToolParameters(params) {
130
146
  }
131
147
  return result;
132
148
  }
133
- /**
134
- * Converts a value to a string representation suitable for MCP responses
135
- * Handles objects and arrays by JSON stringifying them instead of using String()
136
- * @param value - The value to convert to string
137
- * @returns String representation of the value
138
- */
139
- function formatResponseValue(value) {
140
- if (value === null || value === undefined) {
141
- return String(value);
142
- }
143
- if (typeof value === "object") {
144
- try {
145
- return JSON.stringify(value, null, 2);
146
- }
147
- catch (error) {
148
- // Fallback to String() if JSON.stringify fails (e.g., circular references)
149
- return String(value);
150
- }
151
- }
152
- return String(value);
153
- }
154
149
  /**
155
150
  * Constructs a complete Zod schema object for MCP tool input validation
156
151
  * @param params - Record of parameter names to Zod schema types
package/lib/mcp/utils.js CHANGED
@@ -18,6 +18,8 @@ function determineMcpParameterType(cdsType) {
18
18
  return zod_1.z.string();
19
19
  case "Integer":
20
20
  return zod_1.z.number();
21
+ case "Boolean":
22
+ return zod_1.z.boolean();
21
23
  default:
22
24
  return zod_1.z.string();
23
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gavdi/cap-mcp",
3
- "version": "0.9.9",
3
+ "version": "0.10.1",
4
4
  "description": "MCP Pluging for CAP",
5
5
  "keywords": [
6
6
  "MCP",