@gavdi/cap-mcp 1.0.1 → 1.1.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/cds-plugin.js +4 -0
- package/lib/annotations/constants.js +21 -27
- package/lib/annotations/parser.js +43 -48
- package/lib/annotations/utils.js +30 -2
- package/lib/config/build.js +46 -0
- package/lib/config/instructions.js +30 -0
- package/lib/mcp/entity-tools.js +22 -10
- package/lib/mcp/factory.js +4 -2
- package/lib/mcp/utils.js +66 -0
- package/package.json +1 -1
package/cds-plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_ALL_RESOURCE_OPTIONS = exports.
|
|
3
|
+
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
|
|
@@ -11,33 +11,27 @@ exports.DEFAULT_ALL_RESOURCE_OPTIONS = exports.CDS_AUTH_ANNOTATIONS = exports.MC
|
|
|
11
11
|
*/
|
|
12
12
|
exports.MCP_ANNOTATION_KEY = "@mcp";
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
* Maps logical names to their actual annotation keys used in CDS files
|
|
14
|
+
* Mapping of the custom annotations + CDS specific annotations and their correlated mapping for MCP usage
|
|
16
15
|
*/
|
|
17
|
-
exports.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
exports.CDS_AUTH_ANNOTATIONS = {
|
|
38
|
-
REQUIRES: "@requires",
|
|
39
|
-
RESTRICT: "@restrict",
|
|
40
|
-
};
|
|
16
|
+
exports.MCP_ANNOTATION_MAPPING = new Map([
|
|
17
|
+
["@mcp.name", "name"],
|
|
18
|
+
["@mcp.description", "description"],
|
|
19
|
+
["@mcp.resource", "resource"],
|
|
20
|
+
["@mcp.tool", "tool"],
|
|
21
|
+
["@mcp.prompts", "prompts"],
|
|
22
|
+
["@mcp.wrap", "wrap"],
|
|
23
|
+
["@mcp.wrap.tools", "wrap.tools"],
|
|
24
|
+
["@mcp.wrap.modes", "wrap.modes"],
|
|
25
|
+
["@mcp.wrap.hint", "wrap.hint"],
|
|
26
|
+
["@mcp.wrap.hint.get", "wrap.hint.get"],
|
|
27
|
+
["@mcp.wrap.hint.query", "wrap.hint.query"],
|
|
28
|
+
["@mcp.wrap.hint.create", "wrap.hint.create"],
|
|
29
|
+
["@mcp.wrap.hint.update", "wrap.hint.update"],
|
|
30
|
+
["@mcp.wrap.hint.delete", "wrap.hint.delete"],
|
|
31
|
+
["@mcp.elicit", "elicit"],
|
|
32
|
+
["@requires", "requires"],
|
|
33
|
+
["@restrict", "restrict"],
|
|
34
|
+
]);
|
|
41
35
|
/**
|
|
42
36
|
* Default set of all available OData query options for MCP resources
|
|
43
37
|
* Used when @mcp.resource is set to `true` to enable all capabilities
|
|
@@ -22,7 +22,7 @@ function parseDefinitions(model) {
|
|
|
22
22
|
const def = value;
|
|
23
23
|
const parsedAnnotations = parseAnnotations(def);
|
|
24
24
|
const { serviceName, target } = (0, utils_1.splitDefinitionName)(key);
|
|
25
|
-
parseBoundOperations(serviceName, target, def, result); // Mutates result map with bound operations
|
|
25
|
+
parseBoundOperations(model, serviceName, target, def, result); // Mutates result map with bound operations
|
|
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
|
}
|
|
@@ -42,13 +42,13 @@ function parseDefinitions(model) {
|
|
|
42
42
|
result.set(resourceAnnotation.target, resourceAnnotation);
|
|
43
43
|
continue;
|
|
44
44
|
case "function":
|
|
45
|
-
const functionAnnotation = constructToolAnnotation(serviceName, target, verifiedAnnotations);
|
|
45
|
+
const functionAnnotation = constructToolAnnotation(model, serviceName, target, verifiedAnnotations);
|
|
46
46
|
if (!functionAnnotation)
|
|
47
47
|
continue;
|
|
48
48
|
result.set(functionAnnotation.target, functionAnnotation);
|
|
49
49
|
continue;
|
|
50
50
|
case "action":
|
|
51
|
-
const actionAnnotation = constructToolAnnotation(serviceName, target, verifiedAnnotations);
|
|
51
|
+
const actionAnnotation = constructToolAnnotation(model, serviceName, target, verifiedAnnotations);
|
|
52
52
|
if (!actionAnnotation)
|
|
53
53
|
continue;
|
|
54
54
|
result.set(actionAnnotation.target, actionAnnotation);
|
|
@@ -65,6 +65,40 @@ function parseDefinitions(model) {
|
|
|
65
65
|
}
|
|
66
66
|
return result;
|
|
67
67
|
}
|
|
68
|
+
function mapToMcpAnnotationStructure(obj) {
|
|
69
|
+
const result = {};
|
|
70
|
+
// Helper function to set nested properties
|
|
71
|
+
const setNestedValue = (target, path, value) => {
|
|
72
|
+
const keys = path.split(".");
|
|
73
|
+
const lastKey = keys.pop();
|
|
74
|
+
const nestedTarget = keys.reduce((current, key) => {
|
|
75
|
+
if (!(key in current)) {
|
|
76
|
+
current[key] = {};
|
|
77
|
+
}
|
|
78
|
+
return current[key];
|
|
79
|
+
}, target);
|
|
80
|
+
// If the target already has a value and both are objects, merge them
|
|
81
|
+
if (typeof nestedTarget[lastKey] === "object" &&
|
|
82
|
+
typeof value === "object" &&
|
|
83
|
+
nestedTarget[lastKey] !== null &&
|
|
84
|
+
value !== null &&
|
|
85
|
+
!Array.isArray(nestedTarget[lastKey]) &&
|
|
86
|
+
!Array.isArray(value)) {
|
|
87
|
+
nestedTarget[lastKey] = { ...nestedTarget[lastKey], ...value };
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
nestedTarget[lastKey] = value;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
// Loop through object keys and map them
|
|
94
|
+
for (const key in obj) {
|
|
95
|
+
if (constants_1.MCP_ANNOTATION_MAPPING.has(key)) {
|
|
96
|
+
const mappedPath = constants_1.MCP_ANNOTATION_MAPPING.get(key);
|
|
97
|
+
setNestedValue(result, mappedPath, obj[key]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
68
102
|
/**
|
|
69
103
|
* Parses MCP annotations from a definition object
|
|
70
104
|
* @param definition - The definition object to parse annotations from
|
|
@@ -73,50 +107,11 @@ function parseDefinitions(model) {
|
|
|
73
107
|
function parseAnnotations(definition) {
|
|
74
108
|
if (!(0, utils_1.containsMcpAnnotation)(definition))
|
|
75
109
|
return undefined;
|
|
110
|
+
const parsed = mapToMcpAnnotationStructure(definition);
|
|
76
111
|
const annotations = {
|
|
77
112
|
definition: definition,
|
|
113
|
+
...parsed,
|
|
78
114
|
};
|
|
79
|
-
for (const [k, v] of Object.entries(definition)) {
|
|
80
|
-
// Process MCP annotations and CDS auth annotations
|
|
81
|
-
if (!k.includes(constants_1.MCP_ANNOTATION_KEY) &&
|
|
82
|
-
!k.startsWith("@requires") &&
|
|
83
|
-
!k.startsWith("@restrict")) {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
logger_1.LOGGER.debug("Parsing: ", k, v);
|
|
87
|
-
switch (k) {
|
|
88
|
-
case constants_1.MCP_ANNOTATION_PROPS.MCP_NAME:
|
|
89
|
-
annotations.name = v;
|
|
90
|
-
continue;
|
|
91
|
-
case constants_1.MCP_ANNOTATION_PROPS.MCP_DESCRIPTION:
|
|
92
|
-
annotations.description = v;
|
|
93
|
-
continue;
|
|
94
|
-
case constants_1.MCP_ANNOTATION_PROPS.MCP_RESOURCE:
|
|
95
|
-
annotations.resource = v;
|
|
96
|
-
continue;
|
|
97
|
-
case constants_1.MCP_ANNOTATION_PROPS.MCP_TOOL:
|
|
98
|
-
annotations.tool = v;
|
|
99
|
-
continue;
|
|
100
|
-
case constants_1.MCP_ANNOTATION_PROPS.MCP_PROMPT:
|
|
101
|
-
annotations.prompts = v;
|
|
102
|
-
continue;
|
|
103
|
-
case constants_1.MCP_ANNOTATION_PROPS.MCP_WRAP:
|
|
104
|
-
// Wrapper container to expose resources as tools
|
|
105
|
-
annotations.wrap = v;
|
|
106
|
-
continue;
|
|
107
|
-
case constants_1.MCP_ANNOTATION_PROPS.MCP_ELICIT:
|
|
108
|
-
annotations.elicit = v;
|
|
109
|
-
continue;
|
|
110
|
-
case constants_1.CDS_AUTH_ANNOTATIONS.REQUIRES:
|
|
111
|
-
annotations.requires = v;
|
|
112
|
-
continue;
|
|
113
|
-
case constants_1.CDS_AUTH_ANNOTATIONS.RESTRICT:
|
|
114
|
-
annotations.restrict = v;
|
|
115
|
-
continue;
|
|
116
|
-
default:
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
115
|
return annotations;
|
|
121
116
|
}
|
|
122
117
|
/**
|
|
@@ -144,10 +139,10 @@ function constructResourceAnnotation(serviceName, target, annotations, definitio
|
|
|
144
139
|
* @param keyParams - Optional key parameters for bound operations
|
|
145
140
|
* @returns Tool annotation or undefined if invalid
|
|
146
141
|
*/
|
|
147
|
-
function constructToolAnnotation(serviceName, target, annotations, entityKey, keyParams) {
|
|
142
|
+
function constructToolAnnotation(model, serviceName, target, annotations, entityKey, keyParams) {
|
|
148
143
|
if (!(0, utils_1.isValidToolAnnotation)(annotations))
|
|
149
144
|
return undefined;
|
|
150
|
-
const { parameters, operationKind } = (0, utils_1.parseOperationElements)(annotations);
|
|
145
|
+
const { parameters, operationKind } = (0, utils_1.parseOperationElements)(annotations, model);
|
|
151
146
|
const restrictions = (0, utils_1.parseCdsRestrictions)(annotations.restrict, annotations.requires);
|
|
152
147
|
return new structures_1.McpToolAnnotation(annotations.name, annotations.description, target, serviceName, parameters, entityKey, operationKind, keyParams, restrictions, annotations.elicit);
|
|
153
148
|
}
|
|
@@ -170,7 +165,7 @@ function constructPromptAnnotation(serviceName, annotations) {
|
|
|
170
165
|
* @param definition - CSN entity definition containing bound operations
|
|
171
166
|
* @param resultRef - Map to store parsed annotations (mutated by this function)
|
|
172
167
|
*/
|
|
173
|
-
function parseBoundOperations(serviceName, entityKey, definition, resultRef) {
|
|
168
|
+
function parseBoundOperations(model, serviceName, entityKey, definition, resultRef) {
|
|
174
169
|
if (definition.kind !== "entity")
|
|
175
170
|
return;
|
|
176
171
|
const boundOperations = definition
|
|
@@ -192,7 +187,7 @@ function parseBoundOperations(serviceName, entityKey, definition, resultRef) {
|
|
|
192
187
|
continue;
|
|
193
188
|
}
|
|
194
189
|
const verifiedAnnotations = parsedAnnotations;
|
|
195
|
-
const toolAnnotation = constructToolAnnotation(serviceName, k, verifiedAnnotations, entityKey, keyParams);
|
|
190
|
+
const toolAnnotation = constructToolAnnotation(model, serviceName, k, verifiedAnnotations, entityKey, keyParams);
|
|
196
191
|
if (!toolAnnotation)
|
|
197
192
|
continue;
|
|
198
193
|
resultRef.set(k, toolAnnotation);
|
package/lib/annotations/utils.js
CHANGED
|
@@ -175,13 +175,25 @@ function parseResourceElements(definition) {
|
|
|
175
175
|
* @param annotations - The annotation structure to parse
|
|
176
176
|
* @returns Object containing parameters and operation kind
|
|
177
177
|
*/
|
|
178
|
-
function parseOperationElements(annotations) {
|
|
178
|
+
function parseOperationElements(annotations, model) {
|
|
179
179
|
let parameters;
|
|
180
|
+
const parseParam = (k, v, suffix) => {
|
|
181
|
+
if (typeof v.type !== "string") {
|
|
182
|
+
const referencedType = parseTypedReference(v.type, model);
|
|
183
|
+
parameters?.set(k, `${referencedType}${suffix ?? ""}`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
parameters?.set(k, `${v.type.replace("cds.", "")}${suffix ?? ""}`);
|
|
187
|
+
};
|
|
180
188
|
const params = annotations.definition["params"];
|
|
181
189
|
if (params && Object.entries(params).length > 0) {
|
|
182
190
|
parameters = new Map();
|
|
183
191
|
for (const [k, v] of Object.entries(params)) {
|
|
184
|
-
|
|
192
|
+
if (v.items) {
|
|
193
|
+
parseParam(k, v.items, "Array");
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
parseParam(k, v);
|
|
185
197
|
}
|
|
186
198
|
}
|
|
187
199
|
return {
|
|
@@ -189,6 +201,22 @@ function parseOperationElements(annotations) {
|
|
|
189
201
|
operationKind: annotations.definition.kind,
|
|
190
202
|
};
|
|
191
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Recursively digs through the typed reference object of an operation parameter.
|
|
206
|
+
* @param param
|
|
207
|
+
* @param model
|
|
208
|
+
* @returns string|undefined
|
|
209
|
+
* @throws Error if nested type is not parseable
|
|
210
|
+
*/
|
|
211
|
+
function parseTypedReference(param, model) {
|
|
212
|
+
if (!param || !param.ref) {
|
|
213
|
+
throw new Error("Failed to parse nested type reference");
|
|
214
|
+
}
|
|
215
|
+
const referenceType = model.definitions?.[param.ref[0]].elements[param.ref[1]];
|
|
216
|
+
return typeof referenceType?.type === "string"
|
|
217
|
+
? referenceType.type?.replace("cds.", "")
|
|
218
|
+
: parseTypedReference(referenceType?.type, model);
|
|
219
|
+
}
|
|
192
220
|
/**
|
|
193
221
|
* Parses entity keys from a definition
|
|
194
222
|
* @param definition - The definition to parse keys from
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerBuildTask = registerBuildTask;
|
|
4
|
+
const logger_1 = require("../logger");
|
|
5
|
+
const json_parser_1 = require("./json-parser");
|
|
6
|
+
/* @ts-ignore */
|
|
7
|
+
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
|
8
|
+
function registerBuildTask() {
|
|
9
|
+
cds.build?.register("mcp", class McpBuildPlugin extends cds.build.Plugin {
|
|
10
|
+
static taskDefaults = {};
|
|
11
|
+
static instructionsPath;
|
|
12
|
+
static hasTask() {
|
|
13
|
+
const config = cds.env.mcp;
|
|
14
|
+
if (!config) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
else if (typeof config === "object") {
|
|
18
|
+
this.instructionsPath =
|
|
19
|
+
typeof config.instructions === "object"
|
|
20
|
+
? config.instructions.file
|
|
21
|
+
: undefined;
|
|
22
|
+
return (this.instructionsPath !== undefined &&
|
|
23
|
+
this.instructionsPath.length > 0);
|
|
24
|
+
}
|
|
25
|
+
const parsed = (0, json_parser_1.parseCAPConfiguration)(config);
|
|
26
|
+
if (!parsed || typeof parsed.instructions !== "object") {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
this.instructionsPath =
|
|
30
|
+
typeof parsed.instructions === "object"
|
|
31
|
+
? parsed.instructions.file
|
|
32
|
+
: undefined;
|
|
33
|
+
return (this.instructionsPath !== undefined &&
|
|
34
|
+
this.instructionsPath.length > 0);
|
|
35
|
+
}
|
|
36
|
+
async build() {
|
|
37
|
+
logger_1.LOGGER.debug("Performing build task - copy MCP instructions");
|
|
38
|
+
if (!McpBuildPlugin.instructionsPath) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (cds.utils.fs.existsSync(this.task.src, McpBuildPlugin.instructionsPath)) {
|
|
42
|
+
await this.copy(McpBuildPlugin.instructionsPath).to(cds.utils.path.join("srv", McpBuildPlugin.instructionsPath));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getMcpInstructions = getMcpInstructions;
|
|
4
|
+
exports.readInstructionsFile = readInstructionsFile;
|
|
5
|
+
const cds_1 = require("@sap/cds");
|
|
6
|
+
function getMcpInstructions(config) {
|
|
7
|
+
if (!config.instructions) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
if (typeof config.instructions === "string") {
|
|
11
|
+
return config.instructions;
|
|
12
|
+
}
|
|
13
|
+
return config.instructions.file
|
|
14
|
+
? readInstructionsFile(config.instructions.file)
|
|
15
|
+
: undefined;
|
|
16
|
+
}
|
|
17
|
+
function readInstructionsFile(path) {
|
|
18
|
+
if (!containsMarkdownType(path)) {
|
|
19
|
+
throw new Error("Invalid file type provided for instructions");
|
|
20
|
+
}
|
|
21
|
+
else if (!cds_1.utils.fs.existsSync(path)) {
|
|
22
|
+
throw new Error("Instructions file not found");
|
|
23
|
+
}
|
|
24
|
+
const file = cds_1.utils.fs.readFileSync(path);
|
|
25
|
+
return file.toString("utf8");
|
|
26
|
+
}
|
|
27
|
+
function containsMarkdownType(path) {
|
|
28
|
+
const extension = path.substring(path.length - 3);
|
|
29
|
+
return extension === ".md";
|
|
30
|
+
}
|
package/lib/mcp/entity-tools.js
CHANGED
|
@@ -247,8 +247,8 @@ function registerQueryTool(resAnno, server, authEnabled) {
|
|
|
247
247
|
aggregate: inputZod.shape.aggregate,
|
|
248
248
|
explain: inputZod.shape.explain,
|
|
249
249
|
};
|
|
250
|
-
const hint = resAnno
|
|
251
|
-
const desc =
|
|
250
|
+
const hint = constructHintMessage(resAnno, "query");
|
|
251
|
+
const desc = `Resource description: ${resAnno.description}. ${buildEnhancedQueryDescription(resAnno)} CRITICAL: Use foreign key fields (e.g., author_ID) for associations - association names (e.g., author) won't work in filters.` +
|
|
252
252
|
hint;
|
|
253
253
|
const queryHandler = async (rawArgs) => {
|
|
254
254
|
const parsed = inputZod.safeParse(rawArgs);
|
|
@@ -298,8 +298,8 @@ function registerGetTool(resAnno, server, authEnabled) {
|
|
|
298
298
|
inputSchema[k] = (0, utils_2.determineMcpParameterType)(cdsType).describe(`Key ${k}`);
|
|
299
299
|
}
|
|
300
300
|
const keyList = Array.from(resAnno.resourceKeys.keys()).join(", ");
|
|
301
|
-
const hint = resAnno
|
|
302
|
-
const desc = `Get one ${resAnno.target} by key(s): ${keyList}. For fields & examples call cap_describe_model.${hint}`;
|
|
301
|
+
const hint = constructHintMessage(resAnno, "get");
|
|
302
|
+
const desc = `Resource description: ${resAnno.description}. Get one ${resAnno.target} by key(s): ${keyList}. For fields & examples call cap_describe_model.${hint}`;
|
|
303
303
|
const getHandler = async (args) => {
|
|
304
304
|
const startTime = Date.now();
|
|
305
305
|
const CDS = global.cds;
|
|
@@ -380,8 +380,8 @@ function registerCreateTool(resAnno, server, authEnabled) {
|
|
|
380
380
|
.optional()
|
|
381
381
|
.describe(`Field ${propName}`);
|
|
382
382
|
}
|
|
383
|
-
const hint = resAnno
|
|
384
|
-
const desc = `Create a new ${resAnno.target}. Provide fields; service applies defaults.${hint}`;
|
|
383
|
+
const hint = constructHintMessage(resAnno, "create");
|
|
384
|
+
const desc = `Resource description: ${resAnno.description}. Create a new ${resAnno.target}. Provide fields; service applies defaults.${hint}`;
|
|
385
385
|
const createHandler = async (args) => {
|
|
386
386
|
const CDS = global.cds;
|
|
387
387
|
const { INSERT } = CDS.ql;
|
|
@@ -470,8 +470,8 @@ function registerUpdateTool(resAnno, server, authEnabled) {
|
|
|
470
470
|
.describe(`Field ${propName}`);
|
|
471
471
|
}
|
|
472
472
|
const keyList = Array.from(resAnno.resourceKeys.keys()).join(", ");
|
|
473
|
-
const hint = resAnno
|
|
474
|
-
const desc = `Update ${resAnno.target} by key(s): ${keyList}. Provide fields to update.${hint}`;
|
|
473
|
+
const hint = constructHintMessage(resAnno, "update");
|
|
474
|
+
const desc = `Resource description: ${resAnno.description}. Update ${resAnno.target} by key(s): ${keyList}. Provide fields to update.${hint}`;
|
|
475
475
|
const updateHandler = async (args) => {
|
|
476
476
|
const CDS = global.cds;
|
|
477
477
|
const { UPDATE } = CDS.ql;
|
|
@@ -559,8 +559,8 @@ function registerDeleteTool(resAnno, server, authEnabled) {
|
|
|
559
559
|
inputSchema[k] = (0, utils_2.determineMcpParameterType)(cdsType).describe(`Key ${k}`);
|
|
560
560
|
}
|
|
561
561
|
const keyList = Array.from(resAnno.resourceKeys.keys()).join(", ");
|
|
562
|
-
const hint = resAnno
|
|
563
|
-
const desc = `Delete ${resAnno.target} by key(s): ${keyList}. This operation cannot be undone.${hint}`;
|
|
562
|
+
const hint = constructHintMessage(resAnno, "delete");
|
|
563
|
+
const desc = `Resource description: ${resAnno.description}. Delete ${resAnno.target} by key(s): ${keyList}. This operation cannot be undone.${hint}`;
|
|
564
564
|
const deleteHandler = async (args) => {
|
|
565
565
|
const CDS = global.cds;
|
|
566
566
|
const { DELETE } = CDS.ql;
|
|
@@ -703,3 +703,15 @@ async function executeQuery(CDS, svc, args, baseQuery) {
|
|
|
703
703
|
return svc.run(baseQuery);
|
|
704
704
|
}
|
|
705
705
|
}
|
|
706
|
+
function constructHintMessage(resAnno, wrapAction) {
|
|
707
|
+
if (!resAnno.wrap?.hint) {
|
|
708
|
+
return "";
|
|
709
|
+
}
|
|
710
|
+
else if (typeof resAnno.wrap.hint === "string") {
|
|
711
|
+
return ` Hint: ${resAnno.wrap?.hint}`;
|
|
712
|
+
}
|
|
713
|
+
if (typeof resAnno.wrap.hint !== "object") {
|
|
714
|
+
throw new Error(`Unparseable hint provided for entity: ${resAnno.name}`);
|
|
715
|
+
}
|
|
716
|
+
return ` Hint: ${resAnno.wrap.hint[wrapAction] ?? ""}`;
|
|
717
|
+
}
|
package/lib/mcp/factory.js
CHANGED
|
@@ -12,6 +12,7 @@ const utils_1 = require("../auth/utils");
|
|
|
12
12
|
// Use relative import without extension for ts-jest resolver compatibility
|
|
13
13
|
const entity_tools_1 = require("./entity-tools");
|
|
14
14
|
const describe_model_1 = require("./describe-model");
|
|
15
|
+
const instructions_1 = require("../config/instructions");
|
|
15
16
|
/**
|
|
16
17
|
* Creates and configures an MCP server instance with the given configuration and annotations
|
|
17
18
|
* @param config - CAP configuration object
|
|
@@ -24,7 +25,7 @@ function createMcpServer(config, annotations) {
|
|
|
24
25
|
name: config.name,
|
|
25
26
|
version: config.version,
|
|
26
27
|
capabilities: config.capabilities,
|
|
27
|
-
}, { instructions: config
|
|
28
|
+
}, { instructions: (0, instructions_1.getMcpInstructions)(config) });
|
|
28
29
|
if (!annotations) {
|
|
29
30
|
logger_1.LOGGER.debug("No annotations provided, skipping registration...");
|
|
30
31
|
return server;
|
|
@@ -51,7 +52,8 @@ function createMcpServer(config, annotations) {
|
|
|
51
52
|
const localWrap = entry.wrap?.tools;
|
|
52
53
|
const enabled = localWrap === true || (localWrap === undefined && globalWrap);
|
|
53
54
|
if (enabled) {
|
|
54
|
-
const modes =
|
|
55
|
+
const modes = entry.wrap?.modes ??
|
|
56
|
+
config.wrap_entity_modes ?? ["query", "get"];
|
|
55
57
|
(0, entity_tools_1.registerEntityWrappers)(entry, server, authEnabled, modes, accesses);
|
|
56
58
|
}
|
|
57
59
|
continue;
|
package/lib/mcp/utils.js
CHANGED
|
@@ -16,10 +16,76 @@ function determineMcpParameterType(cdsType) {
|
|
|
16
16
|
switch (cdsType) {
|
|
17
17
|
case "String":
|
|
18
18
|
return zod_1.z.string();
|
|
19
|
+
case "UUID":
|
|
20
|
+
return zod_1.z.string();
|
|
21
|
+
case "Date":
|
|
22
|
+
return zod_1.z.date();
|
|
23
|
+
case "Time":
|
|
24
|
+
return zod_1.z.date();
|
|
25
|
+
case "DateTime":
|
|
26
|
+
return zod_1.z.date();
|
|
27
|
+
case "Timestamp":
|
|
28
|
+
return zod_1.z.number();
|
|
19
29
|
case "Integer":
|
|
20
30
|
return zod_1.z.number();
|
|
31
|
+
case "Int16":
|
|
32
|
+
return zod_1.z.number();
|
|
33
|
+
case "Int32":
|
|
34
|
+
return zod_1.z.number();
|
|
35
|
+
case "Int64":
|
|
36
|
+
return zod_1.z.number();
|
|
37
|
+
case "UInt8":
|
|
38
|
+
return zod_1.z.number();
|
|
39
|
+
case "Decimal":
|
|
40
|
+
return zod_1.z.number();
|
|
41
|
+
case "Double":
|
|
42
|
+
return zod_1.z.number();
|
|
21
43
|
case "Boolean":
|
|
22
44
|
return zod_1.z.boolean();
|
|
45
|
+
case "Binary":
|
|
46
|
+
return zod_1.z.string();
|
|
47
|
+
case "LargeBinary":
|
|
48
|
+
return zod_1.z.string();
|
|
49
|
+
case "LargeString":
|
|
50
|
+
return zod_1.z.string();
|
|
51
|
+
case "Map":
|
|
52
|
+
return zod_1.z.any();
|
|
53
|
+
case "StringArray":
|
|
54
|
+
return zod_1.z.array(zod_1.z.string());
|
|
55
|
+
case "DateArray":
|
|
56
|
+
return zod_1.z.array(zod_1.z.date());
|
|
57
|
+
case "TimeArray":
|
|
58
|
+
return zod_1.z.array(zod_1.z.date());
|
|
59
|
+
case "DateTimeArray":
|
|
60
|
+
return zod_1.z.array(zod_1.z.date());
|
|
61
|
+
case "TimestampArray":
|
|
62
|
+
return zod_1.z.array(zod_1.z.number());
|
|
63
|
+
case "UUIDArray":
|
|
64
|
+
return zod_1.z.array(zod_1.z.string());
|
|
65
|
+
case "IntegerArray":
|
|
66
|
+
return zod_1.z.array(zod_1.z.number());
|
|
67
|
+
case "Int16Array":
|
|
68
|
+
return zod_1.z.array(zod_1.z.number());
|
|
69
|
+
case "Int32Array":
|
|
70
|
+
return zod_1.z.array(zod_1.z.number());
|
|
71
|
+
case "Int64Array":
|
|
72
|
+
return zod_1.z.array(zod_1.z.number());
|
|
73
|
+
case "UInt8Array":
|
|
74
|
+
return zod_1.z.array(zod_1.z.number());
|
|
75
|
+
case "DecimalArray":
|
|
76
|
+
return zod_1.z.array(zod_1.z.number());
|
|
77
|
+
case "BooleanArray":
|
|
78
|
+
return zod_1.z.array(zod_1.z.boolean());
|
|
79
|
+
case "DoubleArray":
|
|
80
|
+
return zod_1.z.array(zod_1.z.number());
|
|
81
|
+
case "BinaryArray":
|
|
82
|
+
return zod_1.z.array(zod_1.z.string());
|
|
83
|
+
case "LargeBinaryArray":
|
|
84
|
+
return zod_1.z.array(zod_1.z.string());
|
|
85
|
+
case "LargeStringArray":
|
|
86
|
+
return zod_1.z.array(zod_1.z.string());
|
|
87
|
+
case "MapArray":
|
|
88
|
+
return zod_1.z.array(zod_1.z.any());
|
|
23
89
|
default:
|
|
24
90
|
return zod_1.z.string();
|
|
25
91
|
}
|