@gavdi/cap-mcp 1.0.2 → 1.1.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 +3 -0
- package/cds-plugin.js +4 -0
- package/lib/annotations/constants.js +21 -31
- package/lib/annotations/parser.js +39 -58
- package/lib/annotations/utils.js +42 -16
- package/lib/config/build.js +46 -0
- package/lib/config/instructions.js +30 -0
- package/lib/mcp/describe-model.js +7 -5
- package/lib/mcp/entity-tools.js +36 -16
- package/lib/mcp/factory.js +2 -1
- package/lib/mcp/utils.js +103 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
# CAP MCP Plugin - AI With Ease
|
|
2
|
+
  
|
|
3
|
+
|
|
4
|
+
|
|
2
5
|
|
|
3
6
|
> This implementation is based on the Model Context Protocol (MCP) put forward by Anthropic.
|
|
4
7
|
> For more information on MCP, please have a look at their [official documentation.](https://modelcontextprotocol.io/introduction)
|
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,37 +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
|
-
/**
|
|
38
|
-
* Set of annotations used for CDS auth annotations
|
|
39
|
-
* Maps logical names to their actual annotation keys used in CDS files.
|
|
40
|
-
*/
|
|
41
|
-
exports.CDS_AUTH_ANNOTATIONS = {
|
|
42
|
-
REQUIRES: "@requires",
|
|
43
|
-
RESTRICT: "@restrict",
|
|
44
|
-
};
|
|
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
|
+
]);
|
|
45
35
|
/**
|
|
46
36
|
* Default set of all available OData query options for MCP resources
|
|
47
37
|
* Used when @mcp.resource is set to `true` to enable all capabilities
|
|
@@ -36,7 +36,7 @@ function parseDefinitions(model) {
|
|
|
36
36
|
const verifiedAnnotations = parsedAnnotations;
|
|
37
37
|
switch (def.kind) {
|
|
38
38
|
case "entity":
|
|
39
|
-
const resourceAnnotation = constructResourceAnnotation(serviceName, target, verifiedAnnotations, def);
|
|
39
|
+
const resourceAnnotation = constructResourceAnnotation(serviceName, target, verifiedAnnotations, def, model);
|
|
40
40
|
if (!resourceAnnotation)
|
|
41
41
|
continue;
|
|
42
42
|
result.set(resourceAnnotation.target, resourceAnnotation);
|
|
@@ -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,64 +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_TOOLS:
|
|
104
|
-
if (!annotations.wrap) {
|
|
105
|
-
annotations.wrap = {};
|
|
106
|
-
}
|
|
107
|
-
annotations.wrap.tools = v;
|
|
108
|
-
continue;
|
|
109
|
-
case constants_1.MCP_ANNOTATION_PROPS.MCP_WRAP_HINT:
|
|
110
|
-
if (!annotations.wrap) {
|
|
111
|
-
annotations.wrap = {};
|
|
112
|
-
}
|
|
113
|
-
annotations.wrap.hint = v;
|
|
114
|
-
continue;
|
|
115
|
-
case constants_1.MCP_ANNOTATION_PROPS.MCP_WRAP_MODES:
|
|
116
|
-
if (!annotations.wrap) {
|
|
117
|
-
annotations.wrap = {};
|
|
118
|
-
}
|
|
119
|
-
annotations.wrap.modes = v;
|
|
120
|
-
continue;
|
|
121
|
-
case constants_1.MCP_ANNOTATION_PROPS.MCP_ELICIT:
|
|
122
|
-
annotations.elicit = v;
|
|
123
|
-
continue;
|
|
124
|
-
case constants_1.CDS_AUTH_ANNOTATIONS.REQUIRES:
|
|
125
|
-
annotations.requires = v;
|
|
126
|
-
continue;
|
|
127
|
-
case constants_1.CDS_AUTH_ANNOTATIONS.RESTRICT:
|
|
128
|
-
annotations.restrict = v;
|
|
129
|
-
continue;
|
|
130
|
-
default:
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
115
|
return annotations;
|
|
135
116
|
}
|
|
136
117
|
/**
|
|
@@ -141,11 +122,11 @@ function parseAnnotations(definition) {
|
|
|
141
122
|
* @param definition - CSN definition object
|
|
142
123
|
* @returns Resource annotation or undefined if invalid
|
|
143
124
|
*/
|
|
144
|
-
function constructResourceAnnotation(serviceName, target, annotations, definition) {
|
|
125
|
+
function constructResourceAnnotation(serviceName, target, annotations, definition, model) {
|
|
145
126
|
if (!(0, utils_1.isValidResourceAnnotation)(annotations))
|
|
146
127
|
return undefined;
|
|
147
128
|
const functionalities = (0, utils_1.determineResourceOptions)(annotations);
|
|
148
|
-
const { properties, resourceKeys } = (0, utils_1.parseResourceElements)(definition);
|
|
129
|
+
const { properties, resourceKeys } = (0, utils_1.parseResourceElements)(definition, model);
|
|
149
130
|
const restrictions = (0, utils_1.parseCdsRestrictions)(annotations.restrict, annotations.requires);
|
|
150
131
|
return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys, annotations.wrap, restrictions);
|
|
151
132
|
}
|
package/lib/annotations/utils.js
CHANGED
|
@@ -153,18 +153,42 @@ function determineResourceOptions(annotations) {
|
|
|
153
153
|
* @param definition - The definition to parse
|
|
154
154
|
* @returns Object containing properties and resource keys maps
|
|
155
155
|
*/
|
|
156
|
-
function parseResourceElements(definition) {
|
|
156
|
+
function parseResourceElements(definition, model) {
|
|
157
157
|
const properties = new Map();
|
|
158
158
|
const resourceKeys = new Map();
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
const parseParam = (k, v, suffix) => {
|
|
160
|
+
let result = "";
|
|
161
|
+
if (typeof v.type !== "string") {
|
|
162
|
+
const referencedType = parseTypedReference(v.type, model);
|
|
163
|
+
result = `${referencedType}${suffix ?? ""}`;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
result = `${v.type.replace("cds.", "")}${suffix ?? ""}`;
|
|
167
|
+
}
|
|
168
|
+
properties?.set(k, result);
|
|
169
|
+
return result;
|
|
170
|
+
};
|
|
171
|
+
for (const [k, v] of Object.entries(definition.elements || {})) {
|
|
172
|
+
if (v.items) {
|
|
173
|
+
const result = parseParam(k, v.items, "Array");
|
|
174
|
+
if (!v.key)
|
|
175
|
+
continue;
|
|
176
|
+
resourceKeys.set(k, result);
|
|
161
177
|
continue;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (!
|
|
178
|
+
}
|
|
179
|
+
const result = parseParam(k, v);
|
|
180
|
+
if (!v.key)
|
|
165
181
|
continue;
|
|
166
|
-
resourceKeys.set(
|
|
182
|
+
resourceKeys.set(k, result);
|
|
167
183
|
}
|
|
184
|
+
// for (const [key, value] of Object.entries(definition.elements || {})) {
|
|
185
|
+
// if (!value.type) continue;
|
|
186
|
+
// const parsedType = value.type.replace("cds.", "");
|
|
187
|
+
// properties.set(key, parsedType);
|
|
188
|
+
//
|
|
189
|
+
// if (!value.key) continue;
|
|
190
|
+
// resourceKeys.set(key, parsedType);
|
|
191
|
+
// }
|
|
168
192
|
return {
|
|
169
193
|
properties,
|
|
170
194
|
resourceKeys,
|
|
@@ -177,21 +201,23 @@ function parseResourceElements(definition) {
|
|
|
177
201
|
*/
|
|
178
202
|
function parseOperationElements(annotations, model) {
|
|
179
203
|
let parameters;
|
|
204
|
+
const parseParam = (k, v, suffix) => {
|
|
205
|
+
if (typeof v.type !== "string") {
|
|
206
|
+
const referencedType = parseTypedReference(v.type, model);
|
|
207
|
+
parameters?.set(k, `${referencedType}${suffix ?? ""}`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
parameters?.set(k, `${v.type.replace("cds.", "")}${suffix ?? ""}`);
|
|
211
|
+
};
|
|
180
212
|
const params = annotations.definition["params"];
|
|
181
213
|
if (params && Object.entries(params).length > 0) {
|
|
182
214
|
parameters = new Map();
|
|
183
215
|
for (const [k, v] of Object.entries(params)) {
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
// const typeReference =
|
|
187
|
-
// model.definitions?.[references[0]].elements[references[1]];
|
|
188
|
-
// parameters.set(k, typeReference?.type?.replace("cds.", "") as string);
|
|
189
|
-
const referencedType = parseTypedReference(v.type, model);
|
|
190
|
-
parameters.set(k, referencedType);
|
|
191
|
-
logger_1.LOGGER.debug("Typed reference found", referencedType);
|
|
216
|
+
if (v.items) {
|
|
217
|
+
parseParam(k, v.items, "Array");
|
|
192
218
|
continue;
|
|
193
219
|
}
|
|
194
|
-
|
|
220
|
+
parseParam(k, v);
|
|
195
221
|
}
|
|
196
222
|
}
|
|
197
223
|
return {
|
|
@@ -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
|
+
}
|
|
@@ -30,24 +30,26 @@ function registerDescribeModelTool(server) {
|
|
|
30
30
|
const refl = CDS.reflect(CDS.model);
|
|
31
31
|
const listServices = () => {
|
|
32
32
|
const names = Object.values(CDS.services || {})
|
|
33
|
-
.map((s) => s?.definition?.name || s?.name)
|
|
33
|
+
.map((s) => s?.namespace || s?.definition?.name || s?.name)
|
|
34
34
|
.filter(Boolean);
|
|
35
35
|
return { services: [...new Set(names)].sort() };
|
|
36
36
|
};
|
|
37
37
|
const listEntities = (service) => {
|
|
38
|
-
const all = Object.
|
|
38
|
+
const all = Object.entries(refl.definitions || {})
|
|
39
|
+
.filter((x) => x[1].kind == "entity" && !x[0].startsWith("cds.")) // ignore entities such as "cds.outbox.Messages"
|
|
40
|
+
.map((x) => x[0]);
|
|
39
41
|
const filtered = service
|
|
40
|
-
? all.filter((e) =>
|
|
42
|
+
? all.filter((e) => e.startsWith(service + "."))
|
|
41
43
|
: all;
|
|
42
44
|
return {
|
|
43
|
-
entities: filtered.
|
|
45
|
+
entities: filtered.sort(),
|
|
44
46
|
};
|
|
45
47
|
};
|
|
46
48
|
const describeEntity = (service, entity) => {
|
|
47
49
|
if (!entity)
|
|
48
50
|
return { error: "Please provide 'entity'." };
|
|
49
51
|
const fqn = service && !entity.includes(".") ? `${service}.${entity}` : entity;
|
|
50
|
-
const ent = (refl.
|
|
52
|
+
const ent = (refl.definitions || {})[fqn] || (refl.definitions || {})[entity];
|
|
51
53
|
if (!ent)
|
|
52
54
|
return {
|
|
53
55
|
error: `Entity not found: ${entity}${service ? ` (service ${service})` : ""}`,
|
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;
|
|
@@ -376,12 +376,12 @@ function registerCreateTool(resAnno, server, authEnabled) {
|
|
|
376
376
|
.optional();
|
|
377
377
|
continue;
|
|
378
378
|
}
|
|
379
|
-
inputSchema[propName] = (0, utils_2.determineMcpParameterType)(cdsType)
|
|
379
|
+
inputSchema[propName] = (0, utils_2.determineMcpParameterType)(cdsType, propName, `${resAnno.serviceName}.${resAnno.target}`)
|
|
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;
|
|
@@ -465,13 +465,13 @@ function registerUpdateTool(resAnno, server, authEnabled) {
|
|
|
465
465
|
.optional();
|
|
466
466
|
continue;
|
|
467
467
|
}
|
|
468
|
-
inputSchema[propName] = (0, utils_2.determineMcpParameterType)(cdsType)
|
|
468
|
+
inputSchema[propName] = (0, utils_2.determineMcpParameterType)(cdsType, propName, `${resAnno.serviceName}.${resAnno.target}`)
|
|
469
469
|
.optional()
|
|
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;
|
|
@@ -687,7 +687,11 @@ async function executeQuery(CDS, svc, args, baseQuery) {
|
|
|
687
687
|
const { SELECT } = CDS.ql;
|
|
688
688
|
switch (args.return) {
|
|
689
689
|
case "count": {
|
|
690
|
-
const countQuery = SELECT.from(baseQuery.SELECT.from)
|
|
690
|
+
const countQuery = SELECT.from(baseQuery.SELECT.from)
|
|
691
|
+
.columns("count(1) as count")
|
|
692
|
+
.where(baseQuery.SELECT.where)
|
|
693
|
+
.limit(baseQuery.SELECT.limit?.rows?.val, baseQuery.SELECT.limit?.offset?.val)
|
|
694
|
+
.orderBy(baseQuery.SELECT.orderBy);
|
|
691
695
|
const result = await svc.run(countQuery);
|
|
692
696
|
const row = Array.isArray(result) ? result[0] : result;
|
|
693
697
|
return { count: row?.count ?? 0 };
|
|
@@ -696,10 +700,26 @@ async function executeQuery(CDS, svc, args, baseQuery) {
|
|
|
696
700
|
if (!args.aggregate?.length)
|
|
697
701
|
return [];
|
|
698
702
|
const cols = args.aggregate.map((a) => `${a.fn}(${a.field}) as ${a.fn}_${a.field}`);
|
|
699
|
-
const aggQuery = SELECT.from(baseQuery.SELECT.from)
|
|
700
|
-
|
|
703
|
+
const aggQuery = SELECT.from(baseQuery.SELECT.from)
|
|
704
|
+
.columns(...cols)
|
|
705
|
+
.where(baseQuery.SELECT.where)
|
|
706
|
+
.limit(baseQuery.SELECT.limit?.rows?.val, baseQuery.SELECT.limit?.offset?.val)
|
|
707
|
+
.orderBy(baseQuery.SELECT.orderBy);
|
|
708
|
+
return await svc.run(aggQuery);
|
|
701
709
|
}
|
|
702
710
|
default:
|
|
703
|
-
return svc.run(baseQuery);
|
|
711
|
+
return await svc.run(baseQuery);
|
|
704
712
|
}
|
|
705
713
|
}
|
|
714
|
+
function constructHintMessage(resAnno, wrapAction) {
|
|
715
|
+
if (!resAnno.wrap?.hint) {
|
|
716
|
+
return "";
|
|
717
|
+
}
|
|
718
|
+
else if (typeof resAnno.wrap.hint === "string") {
|
|
719
|
+
return ` Hint: ${resAnno.wrap?.hint}`;
|
|
720
|
+
}
|
|
721
|
+
if (typeof resAnno.wrap.hint !== "object") {
|
|
722
|
+
throw new Error(`Unparseable hint provided for entity: ${resAnno.name}`);
|
|
723
|
+
}
|
|
724
|
+
return ` Hint: ${resAnno.wrap.hint[wrapAction] ?? ""}`;
|
|
725
|
+
}
|
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;
|
package/lib/mcp/utils.js
CHANGED
|
@@ -7,23 +7,125 @@ exports.toolError = toolError;
|
|
|
7
7
|
exports.asMcpResult = asMcpResult;
|
|
8
8
|
const constants_1 = require("./constants");
|
|
9
9
|
const zod_1 = require("zod");
|
|
10
|
+
/* @ts-ignore */
|
|
11
|
+
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
|
10
12
|
/**
|
|
11
13
|
* Converts a CDS type string to the corresponding Zod schema type
|
|
12
14
|
* @param cdsType - The CDS type name (e.g., 'String', 'Integer')
|
|
13
15
|
* @returns Zod schema instance for the given type
|
|
14
16
|
*/
|
|
15
|
-
function determineMcpParameterType(cdsType) {
|
|
17
|
+
function determineMcpParameterType(cdsType, key, target) {
|
|
16
18
|
switch (cdsType) {
|
|
17
19
|
case "String":
|
|
18
20
|
return zod_1.z.string();
|
|
21
|
+
case "UUID":
|
|
22
|
+
return zod_1.z.string();
|
|
23
|
+
case "Date":
|
|
24
|
+
return zod_1.z.date();
|
|
25
|
+
case "Time":
|
|
26
|
+
return zod_1.z.date();
|
|
27
|
+
case "DateTime":
|
|
28
|
+
return zod_1.z.date();
|
|
29
|
+
case "Timestamp":
|
|
30
|
+
return zod_1.z.number();
|
|
19
31
|
case "Integer":
|
|
20
32
|
return zod_1.z.number();
|
|
33
|
+
case "Int16":
|
|
34
|
+
return zod_1.z.number();
|
|
35
|
+
case "Int32":
|
|
36
|
+
return zod_1.z.number();
|
|
37
|
+
case "Int64":
|
|
38
|
+
return zod_1.z.number();
|
|
39
|
+
case "UInt8":
|
|
40
|
+
return zod_1.z.number();
|
|
41
|
+
case "Decimal":
|
|
42
|
+
return zod_1.z.number();
|
|
43
|
+
case "Double":
|
|
44
|
+
return zod_1.z.number();
|
|
21
45
|
case "Boolean":
|
|
22
46
|
return zod_1.z.boolean();
|
|
47
|
+
case "Binary":
|
|
48
|
+
return zod_1.z.string();
|
|
49
|
+
case "LargeBinary":
|
|
50
|
+
return zod_1.z.string();
|
|
51
|
+
case "LargeString":
|
|
52
|
+
return zod_1.z.string();
|
|
53
|
+
case "Map":
|
|
54
|
+
return zod_1.z.object({});
|
|
55
|
+
case "StringArray":
|
|
56
|
+
return zod_1.z.array(zod_1.z.string());
|
|
57
|
+
case "DateArray":
|
|
58
|
+
return zod_1.z.array(zod_1.z.date());
|
|
59
|
+
case "TimeArray":
|
|
60
|
+
return zod_1.z.array(zod_1.z.date());
|
|
61
|
+
case "DateTimeArray":
|
|
62
|
+
return zod_1.z.array(zod_1.z.date());
|
|
63
|
+
case "TimestampArray":
|
|
64
|
+
return zod_1.z.array(zod_1.z.number());
|
|
65
|
+
case "UUIDArray":
|
|
66
|
+
return zod_1.z.array(zod_1.z.string());
|
|
67
|
+
case "IntegerArray":
|
|
68
|
+
return zod_1.z.array(zod_1.z.number());
|
|
69
|
+
case "Int16Array":
|
|
70
|
+
return zod_1.z.array(zod_1.z.number());
|
|
71
|
+
case "Int32Array":
|
|
72
|
+
return zod_1.z.array(zod_1.z.number());
|
|
73
|
+
case "Int64Array":
|
|
74
|
+
return zod_1.z.array(zod_1.z.number());
|
|
75
|
+
case "UInt8Array":
|
|
76
|
+
return zod_1.z.array(zod_1.z.number());
|
|
77
|
+
case "DecimalArray":
|
|
78
|
+
return zod_1.z.array(zod_1.z.number());
|
|
79
|
+
case "BooleanArray":
|
|
80
|
+
return zod_1.z.array(zod_1.z.boolean());
|
|
81
|
+
case "DoubleArray":
|
|
82
|
+
return zod_1.z.array(zod_1.z.number());
|
|
83
|
+
case "BinaryArray":
|
|
84
|
+
return zod_1.z.array(zod_1.z.string());
|
|
85
|
+
case "LargeBinaryArray":
|
|
86
|
+
return zod_1.z.array(zod_1.z.string());
|
|
87
|
+
case "LargeStringArray":
|
|
88
|
+
return zod_1.z.array(zod_1.z.string());
|
|
89
|
+
case "MapArray":
|
|
90
|
+
return zod_1.z.array(zod_1.z.object({}));
|
|
91
|
+
case "Composition":
|
|
92
|
+
return buildCompositionZodType(key, target);
|
|
23
93
|
default:
|
|
24
94
|
return zod_1.z.string();
|
|
25
95
|
}
|
|
26
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Builds the complex ZodType for a CDS type of 'Composition'
|
|
99
|
+
* @param key
|
|
100
|
+
* @param target
|
|
101
|
+
* @returns ZodType
|
|
102
|
+
*/
|
|
103
|
+
function buildCompositionZodType(key, target) {
|
|
104
|
+
const model = cds.model;
|
|
105
|
+
if (!model.definitions || !target || !key) {
|
|
106
|
+
return zod_1.z.object({}); // fallback, might have to reconsider type later
|
|
107
|
+
}
|
|
108
|
+
const targetDef = model.definitions[target];
|
|
109
|
+
const targetProp = targetDef.elements[key];
|
|
110
|
+
const comp = model.definitions[targetProp.target];
|
|
111
|
+
if (!comp) {
|
|
112
|
+
return zod_1.z.object({});
|
|
113
|
+
}
|
|
114
|
+
const isArray = targetProp.cardinality !== undefined;
|
|
115
|
+
const compProperties = new Map();
|
|
116
|
+
for (const [k, v] of Object.entries(comp.elements)) {
|
|
117
|
+
if (!v.type)
|
|
118
|
+
continue;
|
|
119
|
+
const parsedType = v.type.replace("cds.", "");
|
|
120
|
+
if (parsedType === "Association" || parsedType === "Composition")
|
|
121
|
+
continue; // We will not support nested compositions for now
|
|
122
|
+
const isOptional = !v.key && !v.notNull;
|
|
123
|
+
const paramType = determineMcpParameterType(parsedType);
|
|
124
|
+
compProperties.set(k, isOptional ? paramType.optional() : paramType);
|
|
125
|
+
}
|
|
126
|
+
const zodType = zod_1.z.object(Object.fromEntries(compProperties));
|
|
127
|
+
return isArray ? zod_1.z.array(zodType) : zodType;
|
|
128
|
+
}
|
|
27
129
|
/**
|
|
28
130
|
* Handles incoming MCP session requests by validating session IDs and routing to appropriate session
|
|
29
131
|
* @param req - Express request object containing session headers
|