@gavdi/cap-mcp 1.2.1 → 1.3.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
@@ -88,6 +88,8 @@ service CatalogService {
88
88
  }
89
89
  ```
90
90
 
91
+ > **Note**: The `@mcp.wrap.hint` annotation provides operation-level guidance, while `@mcp.hint` on individual elements provides field-level descriptions. Both work together to give AI agents comprehensive context.
92
+
91
93
  ### Step 4: Start Your Application
92
94
 
93
95
  ```bash
@@ -202,6 +204,63 @@ Example:
202
204
  };
203
205
  ```
204
206
 
207
+ For field-level descriptions within these tools, see [Element Hints with @mcp.hint](#element-hints-with-mcphint).
208
+
209
+ ### Omitting Sensitive Fields
210
+
211
+ Protect sensitive data by excluding specific fields from MCP responses using the `@mcp.omit` annotation:
212
+
213
+ ```cds
214
+ namespace my.bookshop;
215
+
216
+ entity Books {
217
+ key ID : Integer;
218
+ title : String;
219
+ stock : Integer;
220
+ author : Association to Authors;
221
+ secretMessage : String @mcp.omit; // Hidden from all MCP responses
222
+ }
223
+
224
+ entity Users {
225
+ key ID : Integer;
226
+ username : String;
227
+ email : String;
228
+ darkestSecret : String @mcp.omit; // Never exposed to MCP clients
229
+ ssn : String @mcp.omit; // Protected sensitive data
230
+ lastLogin : DateTime;
231
+ }
232
+ ```
233
+
234
+ **How It Works:**
235
+ - Fields marked with `@mcp.omit` are automatically filtered from all MCP responses
236
+ - Applies to:
237
+ - **Resources**: Field will not appear in resource read operations
238
+ - **Wrapped Entities**: Omission applies to all entity wrapper operations
239
+
240
+ **Common Use Cases:**
241
+ - **Security**: Hide information sensitive to functionality or business operations
242
+ - **Privacy**: Protect personal identifiers
243
+ - **Internal Data**: Exclude internal notes, audit logs, or system-only fields
244
+ - **Compliance**: Ensure GDPR/CCPA compliance by hiding sensitive personal data
245
+
246
+ **Important Notes:**
247
+ - Omitted fields are **only excluded from outputs** - they can still be provided as inputs for create/update operations
248
+ - The annotation works alongside the CAP standard annotation `@Core.Computed` for comprehensive field control
249
+ - Omitted fields remain queryable in the CAP service - only MCP responses are filtered
250
+
251
+ **Example with Multiple Annotations:**
252
+ ```cds
253
+ entity Products {
254
+ key ID : Integer;
255
+ name : String;
256
+ price : Decimal;
257
+ costPrice : Decimal @mcp.omit; // Hide internal pricing
258
+ createdAt : DateTime @Core.Computed; // Auto-generated, not writable
259
+ updatedAt : DateTime @Core.Computed; // Auto-generated, not writable
260
+ secretNote : String @mcp.omit; // Hide from MCP
261
+ }
262
+ ```
263
+
205
264
  ### Tool Annotations
206
265
 
207
266
  Convert CAP functions and actions into executable AI tools:
@@ -272,6 +331,112 @@ function getBooksByAuthor(authorName: String) returns array of String;
272
331
  - **User Actions**: Accept, decline, or cancel the elicitation request
273
332
  - **Early Exit**: Tools return appropriate messages if declined or cancelled
274
333
 
334
+ ### Element Hints with @mcp.hint
335
+
336
+ Provide contextual descriptions for individual properties and parameters using the `@mcp.hint` annotation. These hints help AI agents better understand the purpose, constraints, and expected values for specific fields.
337
+
338
+ #### Where to Use Hints
339
+
340
+ **Resource Entity Properties**
341
+ ```cds
342
+ entity Books {
343
+ key ID : Integer @mcp.hint: 'Must be a unique number not already in the system';
344
+ title : String;
345
+ stock : Integer @mcp.hint: 'The amount of books currently on store shelves';
346
+ }
347
+ ```
348
+
349
+ **Array Elements**
350
+ ```cds
351
+ entity Authors {
352
+ key ID : Integer;
353
+ name : String @mcp.hint: 'Full name of the author';
354
+ nominations : array of String @mcp.hint: 'Awards that the author has been nominated for';
355
+ }
356
+ ```
357
+
358
+ **Function/Action Parameters**
359
+ ```cds
360
+ @mcp: {
361
+ name : 'books-by-author',
362
+ description: 'Gets a list of books made by the author',
363
+ tool : true
364
+ }
365
+ function getBooksByAuthor(
366
+ authorName : String @mcp.hint: 'Full name of the author you want to get the books of'
367
+ ) returns array of String;
368
+ ```
369
+
370
+ **Complex Type Fields**
371
+ ```cds
372
+ type TValidQuantities {
373
+ positiveOnly : Integer @mcp.hint: 'Only takes in positive numbers, i.e. no negative values such as -1'
374
+ };
375
+ ```
376
+
377
+ #### How Hints Are Used
378
+
379
+ Hints are automatically incorporated into:
380
+ - **Resource Descriptions**: Field-level guidance in entity wrapper tools (query/get/create/update/delete)
381
+ - **Tool Parameter Schemas**: Enhanced parameter descriptions visible to AI agents
382
+ - **Input Validation**: Context for AI agents when constructing function calls
383
+
384
+ #### Example: Enhanced Tool Experience
385
+
386
+ Without `@mcp.hint`:
387
+ ```json
388
+ {
389
+ "tool": "CatalogService_Books_create",
390
+ "parameters": {
391
+ "ID": { "type": "integer" },
392
+ "stock": { "type": "integer" }
393
+ }
394
+ }
395
+ ```
396
+
397
+ With `@mcp.hint`:
398
+ ```json
399
+ {
400
+ "tool": "CatalogService_Books_create",
401
+ "parameters": {
402
+ "ID": {
403
+ "type": "integer",
404
+ "description": "Must be a unique number not already in the system"
405
+ },
406
+ "stock": {
407
+ "type": "integer",
408
+ "description": "The amount of books currently on store shelves"
409
+ }
410
+ }
411
+ }
412
+ ```
413
+
414
+ #### Best Practices
415
+
416
+ 1. **Be Specific**: Provide concrete examples and constraints
417
+ - ❌ Bad: `@mcp.hint: 'Author name'`
418
+ - ✅ Good: `@mcp.hint: 'Full name of the author (e.g., "Ernest Hemingway")'`
419
+
420
+ 2. **Include Constraints**: Document validation rules and business logic
421
+ - ✅ `@mcp.hint: 'Must be between 0 and 999, representing quantity in stock'`
422
+
423
+ 3. **Clarify Foreign Keys**: Help AI agents understand associations
424
+ - ✅ `@mcp.hint: 'Foreign key reference to Authors.ID'`
425
+
426
+ 4. **Explain Business Context**: Add domain-specific information
427
+ - ✅ `@mcp.hint: 'ISBN-13 format, used for unique book identification'`
428
+
429
+ 5. **Avoid Redundancy**: Don't repeat what's obvious from the field name and type
430
+ - ❌ Bad: `stock: Integer @mcp.hint: 'Stock value'`
431
+ - ✅ Good: `stock: Integer @mcp.hint: 'Current inventory count across all warehouses'`
432
+
433
+ #### Technical Notes
434
+
435
+ - Hints are parsed at model load time and stored in the `propertyHints` map
436
+ - Hints work with both simple types and complex nested types
437
+ - Hints are accessible in both resource queries and tool executions
438
+ - Array element hints apply to the array items, not the array itself
439
+
275
440
  ### Prompt Templates
276
441
 
277
442
  Define reusable AI prompt templates:
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_ALL_RESOURCE_OPTIONS = exports.MCP_ANNOTATION_MAPPING = exports.MCP_ANNOTATION_KEY = void 0;
3
+ exports.MCP_OMIT_PROP_KEY = exports.MCP_HINT_ELEMENT = exports.DEFAULT_ALL_RESOURCE_OPTIONS = exports.MCP_ANNOTATION_MAPPING = exports.MCP_ANNOTATION_KEY = void 0;
4
4
  /**
5
5
  * MCP annotation constants and default configurations
6
6
  * Defines the standard annotation keys and default values used throughout the plugin
@@ -44,3 +44,11 @@ exports.DEFAULT_ALL_RESOURCE_OPTIONS = new Set([
44
44
  "skip",
45
45
  "select",
46
46
  ]);
47
+ /**
48
+ * Hint key for annotations made on specific properties/elements
49
+ */
50
+ exports.MCP_HINT_ELEMENT = "@mcp.hint";
51
+ /**
52
+ * MCP omit property annotation key
53
+ */
54
+ exports.MCP_OMIT_PROP_KEY = "@mcp.omit";
@@ -39,19 +39,19 @@ function parseDefinitions(model) {
39
39
  const resourceAnnotation = constructResourceAnnotation(serviceName, target, verifiedAnnotations, def, model);
40
40
  if (!resourceAnnotation)
41
41
  continue;
42
- result.set(resourceAnnotation.target, resourceAnnotation);
42
+ result.set(`${serviceName}.${target}`, resourceAnnotation);
43
43
  continue;
44
44
  case "function":
45
45
  const functionAnnotation = constructToolAnnotation(model, serviceName, target, verifiedAnnotations);
46
46
  if (!functionAnnotation)
47
47
  continue;
48
- result.set(functionAnnotation.target, functionAnnotation);
48
+ result.set(`${serviceName}.${target}`, functionAnnotation);
49
49
  continue;
50
50
  case "action":
51
51
  const actionAnnotation = constructToolAnnotation(model, serviceName, target, verifiedAnnotations);
52
52
  if (!actionAnnotation)
53
53
  continue;
54
- result.set(actionAnnotation.target, actionAnnotation);
54
+ result.set(`${serviceName}.${target}`, actionAnnotation);
55
55
  continue;
56
56
  case "service":
57
57
  const promptsAnnotation = constructPromptAnnotation(serviceName, verifiedAnnotations);
@@ -133,9 +133,14 @@ function constructResourceAnnotation(serviceName, target, annotations, definitio
133
133
  const computedFields = new Set(Object.entries(model.definitions?.[entityTarget].elements ?? {})
134
134
  .filter(([_, v]) => new Map(Object.entries(v).map(([key, value]) => [key.toLowerCase(), value])).get("@core.computed"))
135
135
  .map(([k, _]) => k));
136
- const { properties, resourceKeys } = (0, utils_1.parseResourceElements)(definition, model);
136
+ console.log("I AM TRYING TO PARSE", model.definitions);
137
+ const omittedFields = new Set(Object.entries(model.definitions?.[entityTarget].elements ?? {})
138
+ .filter(([_, v]) => v[constants_1.MCP_OMIT_PROP_KEY])
139
+ .map(([k, _]) => k));
140
+ console.log("OMITTED FIELDS", omittedFields);
141
+ const { properties, resourceKeys, propertyHints } = (0, utils_1.parseResourceElements)(definition, model);
137
142
  const restrictions = (0, utils_1.parseCdsRestrictions)(annotations.restrict, annotations.requires);
138
- return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys, foreignKeys, annotations.wrap, restrictions, computedFields);
143
+ return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys, foreignKeys, annotations.wrap, restrictions, computedFields, propertyHints, omittedFields);
139
144
  }
140
145
  /**
141
146
  * Constructs a tool annotation from parsed annotation data
@@ -149,9 +154,9 @@ function constructResourceAnnotation(serviceName, target, annotations, definitio
149
154
  function constructToolAnnotation(model, serviceName, target, annotations, entityKey, keyParams) {
150
155
  if (!(0, utils_1.isValidToolAnnotation)(annotations))
151
156
  return undefined;
152
- const { parameters, operationKind } = (0, utils_1.parseOperationElements)(annotations, model);
157
+ const { parameters, operationKind, propertyHints } = (0, utils_1.parseOperationElements)(annotations, model);
153
158
  const restrictions = (0, utils_1.parseCdsRestrictions)(annotations.restrict, annotations.requires);
154
- return new structures_1.McpToolAnnotation(annotations.name, annotations.description, target, serviceName, parameters, entityKey, operationKind, keyParams, restrictions, annotations.elicit);
159
+ return new structures_1.McpToolAnnotation(annotations.name, annotations.description, target, serviceName, parameters, entityKey, operationKind, keyParams, restrictions, annotations.elicit, propertyHints);
155
160
  }
156
161
  /**
157
162
  * Constructs a prompt annotation from parsed annotation data
@@ -197,6 +202,6 @@ function parseBoundOperations(model, serviceName, entityKey, definition, resultR
197
202
  const toolAnnotation = constructToolAnnotation(model, serviceName, k, verifiedAnnotations, entityKey, keyParams);
198
203
  if (!toolAnnotation)
199
204
  continue;
200
- resultRef.set(k, toolAnnotation);
205
+ resultRef.set(`${serviceName}.${entityKey}.${k}`, toolAnnotation);
201
206
  }
202
207
  }
@@ -16,6 +16,8 @@ class McpAnnotation {
16
16
  _serviceName;
17
17
  /** Auth roles by providing CDS that is required for use */
18
18
  _restrictions;
19
+ /** Property hints to be used for inputs */
20
+ _propertyHints;
19
21
  /**
20
22
  * Creates a new MCP annotation instance
21
23
  * @param name - Unique identifier for this annotation
@@ -24,12 +26,13 @@ class McpAnnotation {
24
26
  * @param serviceName - Name of the associated CAP service
25
27
  * @param restrictions - Roles required for the given annotation
26
28
  */
27
- constructor(name, description, target, serviceName, restrictions) {
29
+ constructor(name, description, target, serviceName, restrictions, propertyHints) {
28
30
  this._name = name;
29
31
  this._description = description;
30
32
  this._target = target;
31
33
  this._serviceName = serviceName;
32
34
  this._restrictions = restrictions;
35
+ this._propertyHints = propertyHints;
33
36
  }
34
37
  /**
35
38
  * Gets the unique name identifier for this annotation
@@ -67,6 +70,13 @@ class McpAnnotation {
67
70
  get restrictions() {
68
71
  return this._restrictions;
69
72
  }
73
+ /**
74
+ * Gets a map of possible property hints to be used for resource/tool properties.
75
+ * @returns Map of property hints
76
+ */
77
+ get propertyHints() {
78
+ return this._propertyHints;
79
+ }
70
80
  }
71
81
  exports.McpAnnotation = McpAnnotation;
72
82
  /**
@@ -86,6 +96,8 @@ class McpResourceAnnotation extends McpAnnotation {
86
96
  _foreignKeys;
87
97
  /** Set of computed field names */
88
98
  _computedFields;
99
+ /** List of omitted fields */
100
+ _omittedFields;
89
101
  /**
90
102
  * Creates a new MCP resource annotation
91
103
  * @param name - Unique identifier for this resource
@@ -99,15 +111,18 @@ class McpResourceAnnotation extends McpAnnotation {
99
111
  * @param wrap - Wrap usage
100
112
  * @param restrictions - Optional restrictions based on CDS roles
101
113
  * @param computedFields - Optional set of fields that are computed and should be ignored in create scenarios
114
+ * @param propertyHints - Optional map of hints for specific properties on resource
115
+ * @param omittedFields - Optional set of fields that should be omitted from MCP entity
102
116
  */
103
- constructor(name, description, target, serviceName, functionalities, properties, resourceKeys, foreignKeys, wrap, restrictions, computedFields) {
104
- super(name, description, target, serviceName, restrictions ?? []);
117
+ constructor(name, description, target, serviceName, functionalities, properties, resourceKeys, foreignKeys, wrap, restrictions, computedFields, propertyHints, omittedFields) {
118
+ super(name, description, target, serviceName, restrictions ?? [], propertyHints ?? new Map());
105
119
  this._functionalities = functionalities;
106
120
  this._properties = properties;
107
121
  this._resourceKeys = resourceKeys;
108
122
  this._wrap = wrap;
109
123
  this._foreignKeys = foreignKeys;
110
124
  this._computedFields = computedFields;
125
+ this._omittedFields = omittedFields;
111
126
  }
112
127
  /**
113
128
  * Gets the set of enabled OData query functionalities
@@ -149,6 +164,12 @@ class McpResourceAnnotation extends McpAnnotation {
149
164
  get computedFields() {
150
165
  return this._computedFields;
151
166
  }
167
+ /**
168
+ * Gets a set of fields/elements of the resource that should be omitted if any
169
+ */
170
+ get omittedFields() {
171
+ return this._omittedFields;
172
+ }
152
173
  }
153
174
  exports.McpResourceAnnotation = McpResourceAnnotation;
154
175
  /**
@@ -178,9 +199,10 @@ class McpToolAnnotation extends McpAnnotation {
178
199
  * @param keyTypeMap - Optional map of key fields to types for bound operations
179
200
  * @param restrictions - Optional restrictions based on CDS roles
180
201
  * @param elicits - Optional elicited input requirement
202
+ * @param propertyHints - Optional map of property hints for tool inputs
181
203
  */
182
- constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap, restrictions, elicits) {
183
- super(name, description, operation, serviceName, restrictions ?? []);
204
+ constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap, restrictions, elicits, propertyHints) {
205
+ super(name, description, operation, serviceName, restrictions ?? [], propertyHints ?? new Map());
184
206
  this._parameters = parameters;
185
207
  this._entityKey = entityKey;
186
208
  this._operationKind = operationKind;
@@ -239,7 +261,7 @@ class McpPromptAnnotation extends McpAnnotation {
239
261
  * @param prompts - Array of prompt template definitions
240
262
  */
241
263
  constructor(name, description, serviceName, prompts) {
242
- super(name, description, serviceName, serviceName, []);
264
+ super(name, description, serviceName, serviceName, [], new Map());
243
265
  this._prompts = prompts;
244
266
  }
245
267
  /**
@@ -167,6 +167,7 @@ function determineResourceOptions(annotations) {
167
167
  function parseResourceElements(definition, model) {
168
168
  const properties = new Map();
169
169
  const resourceKeys = new Map();
170
+ const propertyHints = new Map();
170
171
  const parseParam = (k, v, suffix) => {
171
172
  let result = "";
172
173
  if (typeof v.type !== "string") {
@@ -176,12 +177,18 @@ function parseResourceElements(definition, model) {
176
177
  else {
177
178
  result = `${v.type.replace("cds.", "")}${suffix ?? ""}`;
178
179
  }
180
+ if (v[constants_1.MCP_HINT_ELEMENT]) {
181
+ propertyHints.set(k, v[constants_1.MCP_HINT_ELEMENT]);
182
+ }
179
183
  properties?.set(k, result);
180
184
  return result;
181
185
  };
182
186
  for (const [k, v] of Object.entries(definition.elements || {})) {
183
187
  if (v.items) {
184
188
  const result = parseParam(k, v.items, "Array");
189
+ if (v[constants_1.MCP_HINT_ELEMENT]) {
190
+ propertyHints.set(k, v[constants_1.MCP_HINT_ELEMENT]);
191
+ }
185
192
  if (!v.key)
186
193
  continue;
187
194
  resourceKeys.set(k, result);
@@ -195,6 +202,7 @@ function parseResourceElements(definition, model) {
195
202
  return {
196
203
  properties,
197
204
  resourceKeys,
205
+ propertyHints,
198
206
  };
199
207
  }
200
208
  /**
@@ -204,12 +212,16 @@ function parseResourceElements(definition, model) {
204
212
  */
205
213
  function parseOperationElements(annotations, model) {
206
214
  let parameters;
215
+ const propertyHints = new Map();
207
216
  const parseParam = (k, v, suffix) => {
208
217
  if (typeof v.type !== "string") {
209
218
  const referencedType = parseTypedReference(v.type, model);
210
219
  parameters?.set(k, `${referencedType}${suffix ?? ""}`);
211
220
  return;
212
221
  }
222
+ if (v[constants_1.MCP_HINT_ELEMENT]) {
223
+ propertyHints.set(k, v[constants_1.MCP_HINT_ELEMENT]);
224
+ }
213
225
  parameters?.set(k, `${v.type.replace("cds.", "")}${suffix ?? ""}`);
214
226
  };
215
227
  const params = annotations.definition["params"];
@@ -218,6 +230,9 @@ function parseOperationElements(annotations, model) {
218
230
  for (const [k, v] of Object.entries(params)) {
219
231
  if (v.items) {
220
232
  parseParam(k, v.items, "Array");
233
+ if (v[constants_1.MCP_HINT_ELEMENT]) {
234
+ propertyHints.set(k, v[constants_1.MCP_HINT_ELEMENT]);
235
+ }
221
236
  continue;
222
237
  }
223
238
  parseParam(k, v);
@@ -226,6 +241,7 @@ function parseOperationElements(annotations, model) {
226
241
  return {
227
242
  parameters,
228
243
  operationKind: annotations.definition.kind,
244
+ propertyHints,
229
245
  };
230
246
  }
231
247
  /**
@@ -139,7 +139,8 @@ function registerQueryTool(resAnno, server, authEnabled) {
139
139
  // Structured input schema for queries with guard for empty property lists
140
140
  const allKeys = Array.from(resAnno.properties.keys());
141
141
  const scalarKeys = Array.from(resAnno.properties.entries())
142
- .filter(([, cdsType]) => !String(cdsType).toLowerCase().includes("association"))
142
+ .filter(([k, cdsType]) => !String(cdsType).toLowerCase().includes("association") &&
143
+ !resAnno.omittedFields?.has(k))
143
144
  .map(([name]) => name);
144
145
  // Build where field enum: use same fields as select (scalar + foreign keys)
145
146
  // This ensures consistency - what you can select, you can filter by
@@ -252,8 +253,9 @@ function registerQueryTool(resAnno, server, authEnabled) {
252
253
  try {
253
254
  const t0 = Date.now();
254
255
  const response = await withTimeout(executeQuery(CDS, svc, args, q), TIMEOUT_MS, toolName);
256
+ const result = response?.map((obj) => (0, utils_2.applyOmissionFilter)(obj, resAnno));
255
257
  logger_1.LOGGER.debug(`[EXECUTION TIME] Query tool completed: ${toolName} in ${Date.now() - t0}ms`, { resultKind: args.return ?? "rows" });
256
- return (0, utils_2.asMcpResult)(args.explain ? { data: response, plan: undefined } : response);
258
+ return (0, utils_2.asMcpResult)(args.explain ? { data: result, plan: undefined } : result);
257
259
  }
258
260
  catch (error) {
259
261
  const msg = `QUERY_FAILED: ${error?.message || String(error)}`;
@@ -271,7 +273,7 @@ function registerGetTool(resAnno, server, authEnabled) {
271
273
  const toolName = nameFor(resAnno.serviceName, resAnno.target, "get");
272
274
  const inputSchema = {};
273
275
  for (const [k, cdsType] of resAnno.resourceKeys.entries()) {
274
- inputSchema[k] = (0, utils_2.determineMcpParameterType)(cdsType).describe(`Key ${k}`);
276
+ inputSchema[k] = (0, utils_2.determineMcpParameterType)(cdsType).describe(`Key ${k}. ${resAnno.propertyHints.get(k) ?? ""}`);
275
277
  }
276
278
  const keyList = Array.from(resAnno.resourceKeys.keys()).join(", ");
277
279
  const hint = constructHintMessage(resAnno, "get");
@@ -323,9 +325,10 @@ function registerGetTool(resAnno, server, authEnabled) {
323
325
  }
324
326
  logger_1.LOGGER.debug(`Executing READ on ${resAnno.target} with keys`, keys);
325
327
  try {
326
- const response = await withTimeout(svc.run(svc.read(resAnno.target, keys)), TIMEOUT_MS, `${toolName}`);
328
+ let response = await withTimeout(svc.run(svc.read(resAnno.target, keys)), TIMEOUT_MS, `${toolName}`);
327
329
  logger_1.LOGGER.debug(`[EXECUTION TIME] Get tool completed: ${toolName} in ${Date.now() - startTime}ms`, { found: !!response });
328
- return (0, utils_2.asMcpResult)(response ?? null);
330
+ const result = (0, utils_2.applyOmissionFilter)(response, resAnno);
331
+ return (0, utils_2.asMcpResult)(result ?? null);
329
332
  }
330
333
  catch (error) {
331
334
  const msg = `GET_FAILED: ${error?.message || String(error)}`;
@@ -352,8 +355,8 @@ function registerCreateTool(resAnno, server, authEnabled) {
352
355
  inputSchema[propName] = (0, utils_2.determineMcpParameterType)(cdsType, propName, `${resAnno.serviceName}.${resAnno.target}`)
353
356
  .optional()
354
357
  .describe(resAnno.foreignKeys.has(propName)
355
- ? `Foreign key to ${resAnno.foreignKeys.get(propName)} on ${propName}`
356
- : `Field ${propName}`);
358
+ ? `Foreign key to ${resAnno.foreignKeys.get(propName)} on ${propName}. ${resAnno.propertyHints.get(propName) ?? ""}`
359
+ : `Field ${propName}. ${resAnno.propertyHints.get(propName) ?? ""}`);
357
360
  }
358
361
  const hint = constructHintMessage(resAnno, "create");
359
362
  const desc = `Resource description: ${resAnno.description}. Create a new ${resAnno.target}. Provide fields; service applies defaults.${hint}`;
@@ -400,7 +403,8 @@ function registerCreateTool(resAnno, server, authEnabled) {
400
403
  await tx.commit();
401
404
  }
402
405
  catch { }
403
- return (0, utils_2.asMcpResult)(response ?? {});
406
+ const result = (0, utils_2.applyOmissionFilter)(response, resAnno);
407
+ return (0, utils_2.asMcpResult)(result ?? {});
404
408
  }
405
409
  catch (error) {
406
410
  try {
@@ -426,7 +430,7 @@ function registerUpdateTool(resAnno, server, authEnabled) {
426
430
  const inputSchema = {};
427
431
  // Keys required
428
432
  for (const [k, cdsType] of resAnno.resourceKeys.entries()) {
429
- inputSchema[k] = (0, utils_2.determineMcpParameterType)(cdsType).describe(`Key ${k}`);
433
+ inputSchema[k] = (0, utils_2.determineMcpParameterType)(cdsType).describe(`Key ${k}. ${resAnno.propertyHints.get(k) ?? ""}`);
430
434
  }
431
435
  // Other fields optional
432
436
  for (const [propName, cdsType] of resAnno.properties.entries()) {
@@ -441,8 +445,8 @@ function registerUpdateTool(resAnno, server, authEnabled) {
441
445
  inputSchema[propName] = (0, utils_2.determineMcpParameterType)(cdsType, propName, `${resAnno.serviceName}.${resAnno.target}`)
442
446
  .optional()
443
447
  .describe(resAnno.foreignKeys.has(propName)
444
- ? `Foreign key to ${resAnno.foreignKeys.get(propName)} on ${propName}`
445
- : `Field ${propName}`);
448
+ ? `Foreign key to ${resAnno.foreignKeys.get(propName)} on ${propName}. ${resAnno.propertyHints.get(propName) ?? ""}`
449
+ : `Field ${propName}. ${resAnno.propertyHints.get(propName) ?? ""}`);
446
450
  }
447
451
  const keyList = Array.from(resAnno.resourceKeys.keys()).join(", ");
448
452
  const hint = constructHintMessage(resAnno, "update");
@@ -505,7 +509,8 @@ function registerUpdateTool(resAnno, server, authEnabled) {
505
509
  await tx.commit();
506
510
  }
507
511
  catch { }
508
- return (0, utils_2.asMcpResult)(response ?? {});
512
+ const result = (0, utils_2.applyOmissionFilter)(response, resAnno);
513
+ return (0, utils_2.asMcpResult)(result ?? {});
509
514
  }
510
515
  catch (error) {
511
516
  try {
@@ -531,7 +536,7 @@ function registerDeleteTool(resAnno, server, authEnabled) {
531
536
  const inputSchema = {};
532
537
  // Keys required for deletion
533
538
  for (const [k, cdsType] of resAnno.resourceKeys.entries()) {
534
- inputSchema[k] = (0, utils_2.determineMcpParameterType)(cdsType).describe(`Key ${k}`);
539
+ inputSchema[k] = (0, utils_2.determineMcpParameterType)(cdsType).describe(`Key ${k}. ${resAnno.propertyHints.get(k) ?? ""}`);
535
540
  }
536
541
  const keyList = Array.from(resAnno.resourceKeys.keys()).join(", ");
537
542
  const hint = constructHintMessage(resAnno, "delete");
@@ -101,11 +101,12 @@ function assignResourceToServer(model, server, authEnabled) {
101
101
  try {
102
102
  const accessRights = (0, utils_2.getAccessRights)(authEnabled);
103
103
  const response = await service.tx({ user: accessRights }).run(query);
104
+ const result = response?.map((el) => (0, utils_1.applyOmissionFilter)(el, model));
104
105
  return {
105
106
  contents: [
106
107
  {
107
108
  uri: uri.href,
108
- text: response ? JSON.stringify(response) : "",
109
+ text: result ? JSON.stringify(result) : "",
109
110
  },
110
111
  ],
111
112
  };
@@ -141,11 +142,12 @@ function registerStaticResource(model, server, authEnabled) {
141
142
  : 100);
142
143
  const accessRights = (0, utils_2.getAccessRights)(authEnabled);
143
144
  const response = await service.tx({ user: accessRights }).run(query);
145
+ const result = response?.map((el) => (0, utils_1.applyOmissionFilter)(el, model));
144
146
  return {
145
147
  contents: [
146
148
  {
147
149
  uri: uri.href,
148
- text: response ? JSON.stringify(response) : "",
150
+ text: result ? JSON.stringify(result) : "",
149
151
  },
150
152
  ],
151
153
  };
package/lib/mcp/tools.js CHANGED
@@ -17,7 +17,7 @@ const cds = global.cds || require("@sap/cds"); // This is a work around for miss
17
17
  */
18
18
  function assignToolToServer(model, server, authEnabled) {
19
19
  logger_1.LOGGER.debug("Adding tool", model);
20
- const parameters = buildToolParameters(model.parameters);
20
+ const parameters = buildToolParameters(model.parameters, model.propertyHints);
21
21
  if (model.entityKey) {
22
22
  // Assign tool as bound operation
23
23
  assignBoundOperation(parameters, model, server, authEnabled);
@@ -37,7 +37,7 @@ function assignBoundOperation(params, model, server, authEnabled) {
37
37
  logger_1.LOGGER.error("Invalid tool assignment - missing key map for bound operation");
38
38
  throw new Error("Bound operation cannot be assigned to tool list, missing keys");
39
39
  }
40
- const keys = buildToolParameters(model.keyTypeMap);
40
+ const keys = buildToolParameters(model.keyTypeMap, model.propertyHints);
41
41
  const useElicitInput = (0, elicited_input_1.isElicitInput)(model.elicits);
42
42
  const inputSchema = buildZodSchema({
43
43
  ...keys,
@@ -137,12 +137,12 @@ function assignUnboundOperation(params, model, server, authEnabled) {
137
137
  * @param params - Map of parameter names to their CDS type strings
138
138
  * @returns Record of parameter names to Zod schema types
139
139
  */
140
- function buildToolParameters(params) {
140
+ function buildToolParameters(params, propertyHints) {
141
141
  if (!params || params.size <= 0)
142
142
  return {};
143
143
  const result = {};
144
144
  for (const [k, v] of params.entries()) {
145
- result[k] = (0, utils_1.determineMcpParameterType)(v);
145
+ result[k] = (0, utils_1.determineMcpParameterType)(v)?.describe(propertyHints.get(k) ?? "");
146
146
  }
147
147
  return result;
148
148
  }
package/lib/mcp/utils.js CHANGED
@@ -5,6 +5,7 @@ exports.handleMcpSessionRequest = handleMcpSessionRequest;
5
5
  exports.writeODataDescriptionForResource = writeODataDescriptionForResource;
6
6
  exports.toolError = toolError;
7
7
  exports.asMcpResult = asMcpResult;
8
+ exports.applyOmissionFilter = applyOmissionFilter;
8
9
  const constants_1 = require("./constants");
9
10
  const zod_1 = require("zod");
10
11
  /* @ts-ignore */
@@ -116,6 +117,27 @@ function buildCompositionZodType(key, target) {
116
117
  for (const [k, v] of Object.entries(comp.elements)) {
117
118
  if (!v.type)
118
119
  continue;
120
+ const elementKeys = new Map(Object.keys(v).map((el) => [el.toLowerCase(), el]));
121
+ const isComputed = elementKeys.has("@core.computed") &&
122
+ v[elementKeys.get("@core.computed") ?? ""] === true;
123
+ if (isComputed)
124
+ continue;
125
+ // Check if this field is a foreign key to the parent entity in the composition
126
+ // If so, exclude it because CAP will auto-fill it during deep insert
127
+ const foreignKeyAnnotation = elementKeys.has("@odata.foreignkey4")
128
+ ? elementKeys.get("@odata.foreignkey4")
129
+ : null;
130
+ if (foreignKeyAnnotation) {
131
+ const associationName = v[foreignKeyAnnotation];
132
+ // Check if the association references the parent entity
133
+ if (associationName && comp.elements[associationName]) {
134
+ const association = comp.elements[associationName];
135
+ if (association.target === target) {
136
+ // This FK references the parent entity, exclude it from composition schema
137
+ continue;
138
+ }
139
+ }
140
+ }
119
141
  const parsedType = v.type.replace("cds.", "");
120
142
  if (parsedType === "Association" || parsedType === "Composition")
121
143
  continue; // We will not support nested compositions for now
@@ -229,3 +251,18 @@ function asMcpResult(payload) {
229
251
  ],
230
252
  };
231
253
  }
254
+ /**
255
+ * Applies the omit rules for the resulting object based on the annotations.
256
+ * Creates a copy of the input object to avoid unwanted mutations.
257
+ * @param res
258
+ * @param annotations
259
+ * @returns object|undefined
260
+ */
261
+ function applyOmissionFilter(res, annotations) {
262
+ if (!res)
263
+ return res; // We do not want to parse something that does not exist
264
+ else if (!annotations.omittedFields || annotations.omittedFields.size < 0) {
265
+ return { ...res };
266
+ }
267
+ return Object.fromEntries(Object.entries(res).filter(([k, _]) => !annotations.omittedFields?.has(k)));
268
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gavdi/cap-mcp",
3
- "version": "1.2.1",
4
- "description": "MCP Pluging for CAP",
3
+ "version": "1.3.0",
4
+ "description": "MCP Plugin for CAP",
5
5
  "keywords": [
6
6
  "MCP",
7
7
  "CAP",