@gavdi/cap-mcp 1.1.0 → 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/lib/annotations/parser.js +3 -3
- package/lib/annotations/utils.js +31 -7
- package/lib/mcp/describe-model.js +7 -5
- package/lib/mcp/entity-tools.js +14 -6
- package/lib/mcp/utils.js +39 -3
- 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)
|
|
@@ -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);
|
|
@@ -122,11 +122,11 @@ function parseAnnotations(definition) {
|
|
|
122
122
|
* @param definition - CSN definition object
|
|
123
123
|
* @returns Resource annotation or undefined if invalid
|
|
124
124
|
*/
|
|
125
|
-
function constructResourceAnnotation(serviceName, target, annotations, definition) {
|
|
125
|
+
function constructResourceAnnotation(serviceName, target, annotations, definition, model) {
|
|
126
126
|
if (!(0, utils_1.isValidResourceAnnotation)(annotations))
|
|
127
127
|
return undefined;
|
|
128
128
|
const functionalities = (0, utils_1.determineResourceOptions)(annotations);
|
|
129
|
-
const { properties, resourceKeys } = (0, utils_1.parseResourceElements)(definition);
|
|
129
|
+
const { properties, resourceKeys } = (0, utils_1.parseResourceElements)(definition, model);
|
|
130
130
|
const restrictions = (0, utils_1.parseCdsRestrictions)(annotations.restrict, annotations.requires);
|
|
131
131
|
return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys, annotations.wrap, restrictions);
|
|
132
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,
|
|
@@ -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
|
@@ -376,7 +376,7 @@ 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
|
}
|
|
@@ -465,7 +465,7 @@ 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
|
}
|
|
@@ -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,11 +700,15 @@ 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
|
}
|
|
706
714
|
function constructHintMessage(resAnno, wrapAction) {
|
package/lib/mcp/utils.js
CHANGED
|
@@ -7,12 +7,14 @@ 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();
|
|
@@ -49,7 +51,7 @@ function determineMcpParameterType(cdsType) {
|
|
|
49
51
|
case "LargeString":
|
|
50
52
|
return zod_1.z.string();
|
|
51
53
|
case "Map":
|
|
52
|
-
return zod_1.z.
|
|
54
|
+
return zod_1.z.object({});
|
|
53
55
|
case "StringArray":
|
|
54
56
|
return zod_1.z.array(zod_1.z.string());
|
|
55
57
|
case "DateArray":
|
|
@@ -85,11 +87,45 @@ function determineMcpParameterType(cdsType) {
|
|
|
85
87
|
case "LargeStringArray":
|
|
86
88
|
return zod_1.z.array(zod_1.z.string());
|
|
87
89
|
case "MapArray":
|
|
88
|
-
return zod_1.z.array(zod_1.z.
|
|
90
|
+
return zod_1.z.array(zod_1.z.object({}));
|
|
91
|
+
case "Composition":
|
|
92
|
+
return buildCompositionZodType(key, target);
|
|
89
93
|
default:
|
|
90
94
|
return zod_1.z.string();
|
|
91
95
|
}
|
|
92
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
|
+
}
|
|
93
129
|
/**
|
|
94
130
|
* Handles incoming MCP session requests by validating session IDs and routing to appropriate session
|
|
95
131
|
* @param req - Express request object containing session headers
|