@gavdi/cap-mcp 1.2.2 → 1.3.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 +165 -0
- package/lib/annotations/constants.js +9 -1
- package/lib/annotations/parser.js +7 -4
- package/lib/annotations/structures.js +28 -6
- package/lib/annotations/utils.js +16 -0
- package/lib/mcp/entity-tools.js +18 -13
- package/lib/mcp/resources.js +4 -2
- package/lib/mcp/tools.js +4 -4
- package/lib/mcp/utils.js +37 -0
- package/package.json +2 -2
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";
|
|
@@ -133,9 +133,12 @@ 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
|
|
136
|
+
const omittedFields = new Set(Object.entries(model.definitions?.[entityTarget].elements ?? {})
|
|
137
|
+
.filter(([_, v]) => v[constants_1.MCP_OMIT_PROP_KEY])
|
|
138
|
+
.map(([k, _]) => k));
|
|
139
|
+
const { properties, resourceKeys, propertyHints } = (0, utils_1.parseResourceElements)(definition, model);
|
|
137
140
|
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);
|
|
141
|
+
return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys, foreignKeys, annotations.wrap, restrictions, computedFields, propertyHints, omittedFields);
|
|
139
142
|
}
|
|
140
143
|
/**
|
|
141
144
|
* Constructs a tool annotation from parsed annotation data
|
|
@@ -149,9 +152,9 @@ function constructResourceAnnotation(serviceName, target, annotations, definitio
|
|
|
149
152
|
function constructToolAnnotation(model, serviceName, target, annotations, entityKey, keyParams) {
|
|
150
153
|
if (!(0, utils_1.isValidToolAnnotation)(annotations))
|
|
151
154
|
return undefined;
|
|
152
|
-
const { parameters, operationKind } = (0, utils_1.parseOperationElements)(annotations, model);
|
|
155
|
+
const { parameters, operationKind, propertyHints } = (0, utils_1.parseOperationElements)(annotations, model);
|
|
153
156
|
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);
|
|
157
|
+
return new structures_1.McpToolAnnotation(annotations.name, annotations.description, target, serviceName, parameters, entityKey, operationKind, keyParams, restrictions, annotations.elicit, propertyHints);
|
|
155
158
|
}
|
|
156
159
|
/**
|
|
157
160
|
* Constructs a prompt annotation from parsed annotation data
|
|
@@ -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
|
/**
|
package/lib/annotations/utils.js
CHANGED
|
@@ -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
|
/**
|
package/lib/mcp/entity-tools.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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");
|
package/lib/mcp/resources.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
+
}
|