@gavdi/cap-mcp 0.9.9-alpha.3 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -18
- package/lib/annotations/constants.js +2 -0
- package/lib/annotations/parser.js +18 -2
- package/lib/annotations/structures.js +12 -1
- package/lib/annotations/utils.js +16 -0
- package/lib/auth/handler.js +1 -13
- package/lib/auth/utils.js +51 -79
- package/lib/mcp/describe-model.js +6 -2
- package/lib/mcp/elicited-input.js +162 -0
- package/lib/mcp/entity-tools.js +98 -21
- package/lib/mcp/tools.js +20 -25
- package/lib/mcp/utils.js +2 -0
- package/lib/mcp.js +1 -3
- package/package.json +2 -2
- package/lib/.DS_Store +0 -0
- package/lib/annotations.js +0 -257
- package/lib/auth/adapter.js +0 -2
- package/lib/auth/mock.js +0 -2
- package/lib/auth/types.js +0 -2
- package/lib/mcp/customResourceTemplate.js +0 -156
- package/lib/types.js +0 -2
- package/lib/utils.js +0 -136
package/lib/mcp/entity-tools.js
CHANGED
|
@@ -61,6 +61,45 @@ async function resolveServiceInstance(serviceName) {
|
|
|
61
61
|
// NOTE: We use plain entity names (service projection) for queries.
|
|
62
62
|
const MAX_TOP = 200;
|
|
63
63
|
const TIMEOUT_MS = 10_000; // Standard timeout for tool calls (ms)
|
|
64
|
+
// Map OData operators to CDS/SQL operators for better performance and readability
|
|
65
|
+
const ODATA_TO_CDS_OPERATORS = new Map([
|
|
66
|
+
["eq", "="],
|
|
67
|
+
["ne", "!="],
|
|
68
|
+
["gt", ">"],
|
|
69
|
+
["ge", ">="],
|
|
70
|
+
["lt", "<"],
|
|
71
|
+
["le", "<="],
|
|
72
|
+
]);
|
|
73
|
+
/**
|
|
74
|
+
* Builds enhanced query tool description with field types and association examples
|
|
75
|
+
*/
|
|
76
|
+
function buildEnhancedQueryDescription(resAnno) {
|
|
77
|
+
const associations = Array.from(resAnno.properties.entries())
|
|
78
|
+
.filter(([, cdsType]) => String(cdsType).toLowerCase().includes("association"))
|
|
79
|
+
.map(([name]) => `${name}_ID`);
|
|
80
|
+
const baseDesc = `Query ${resAnno.target} with structured filters, select, orderby, top/skip.`;
|
|
81
|
+
const assocHint = associations.length > 0
|
|
82
|
+
? ` IMPORTANT: For associations, always use foreign key fields (${associations.join(", ")}) - never use association names directly.`
|
|
83
|
+
: "";
|
|
84
|
+
return baseDesc + assocHint;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Builds field documentation for schema descriptions
|
|
88
|
+
*/
|
|
89
|
+
function buildFieldDocumentation(resAnno) {
|
|
90
|
+
const docs = [];
|
|
91
|
+
for (const [propName, cdsType] of resAnno.properties.entries()) {
|
|
92
|
+
const isAssociation = String(cdsType).toLowerCase().includes("association");
|
|
93
|
+
if (isAssociation) {
|
|
94
|
+
docs.push(`${propName}(association: compare by key value)`);
|
|
95
|
+
docs.push(`${propName}_ID(foreign key for ${propName})`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
docs.push(`${propName}(${String(cdsType).toLowerCase()})`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return docs.join(", ");
|
|
102
|
+
}
|
|
64
103
|
/**
|
|
65
104
|
* Registers CRUD-like MCP tools for an annotated entity (resource).
|
|
66
105
|
* Modes can be controlled globally via configuration and per-entity via @mcp.wrap.
|
|
@@ -115,9 +154,27 @@ function nameFor(service, entity, suffix) {
|
|
|
115
154
|
function registerQueryTool(resAnno, server, authEnabled) {
|
|
116
155
|
const toolName = nameFor(resAnno.serviceName, resAnno.target, "query");
|
|
117
156
|
// Structured input schema for queries with guard for empty property lists
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
157
|
+
const allKeys = Array.from(resAnno.properties.keys());
|
|
158
|
+
const scalarKeys = Array.from(resAnno.properties.entries())
|
|
159
|
+
.filter(([, cdsType]) => !String(cdsType).toLowerCase().includes("association"))
|
|
160
|
+
.map(([name]) => name);
|
|
161
|
+
// Add foreign key fields for associations to scalar keys for select/orderby
|
|
162
|
+
for (const [propName, cdsType] of resAnno.properties.entries()) {
|
|
163
|
+
const isAssociation = String(cdsType).toLowerCase().includes("association");
|
|
164
|
+
if (isAssociation) {
|
|
165
|
+
scalarKeys.push(`${propName}_ID`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Build where field enum: use same fields as select (scalar + foreign keys)
|
|
169
|
+
// This ensures consistency - what you can select, you can filter by
|
|
170
|
+
const whereKeys = [...scalarKeys];
|
|
171
|
+
const whereFieldEnum = (whereKeys.length
|
|
172
|
+
? zod_1.z.enum(whereKeys)
|
|
173
|
+
: zod_1.z
|
|
174
|
+
.enum(["__dummy__"])
|
|
175
|
+
.transform(() => "__dummy__"));
|
|
176
|
+
const selectFieldEnum = (scalarKeys.length
|
|
177
|
+
? zod_1.z.enum(scalarKeys)
|
|
121
178
|
: zod_1.z
|
|
122
179
|
.enum(["__dummy__"])
|
|
123
180
|
.transform(() => "__dummy__"));
|
|
@@ -131,16 +188,21 @@ function registerQueryTool(resAnno, server, authEnabled) {
|
|
|
131
188
|
.default(25)
|
|
132
189
|
.describe("Rows (default 25)"),
|
|
133
190
|
skip: zod_1.z.number().int().min(0).default(0).describe("Offset"),
|
|
134
|
-
select: zod_1.z
|
|
191
|
+
select: zod_1.z
|
|
192
|
+
.array(selectFieldEnum)
|
|
193
|
+
.optional()
|
|
194
|
+
.transform((val) => val && val.length > 0 ? val : undefined)
|
|
195
|
+
.describe(`Select/orderby allow only scalar fields: ${scalarKeys.join(", ")}`),
|
|
135
196
|
orderby: zod_1.z
|
|
136
197
|
.array(zod_1.z.object({
|
|
137
|
-
field:
|
|
198
|
+
field: selectFieldEnum,
|
|
138
199
|
dir: zod_1.z.enum(["asc", "desc"]).default("asc"),
|
|
139
200
|
}))
|
|
140
|
-
.optional()
|
|
201
|
+
.optional()
|
|
202
|
+
.transform((val) => val && val.length > 0 ? val : undefined),
|
|
141
203
|
where: zod_1.z
|
|
142
204
|
.array(zod_1.z.object({
|
|
143
|
-
field:
|
|
205
|
+
field: whereFieldEnum.describe(`FILTERABLE FIELDS: ${scalarKeys.join(", ")}. For associations use foreign key (author_ID), NOT association name (author).`),
|
|
144
206
|
op: zod_1.z.enum([
|
|
145
207
|
"eq",
|
|
146
208
|
"ne",
|
|
@@ -160,15 +222,17 @@ function registerQueryTool(resAnno, server, authEnabled) {
|
|
|
160
222
|
zod_1.z.array(zod_1.z.union([zod_1.z.string(), zod_1.z.number()])),
|
|
161
223
|
]),
|
|
162
224
|
}))
|
|
163
|
-
.optional()
|
|
225
|
+
.optional()
|
|
226
|
+
.transform((val) => val && val.length > 0 ? val : undefined),
|
|
164
227
|
q: zod_1.z.string().optional().describe("Quick text search"),
|
|
165
228
|
return: zod_1.z.enum(["rows", "count", "aggregate"]).default("rows").optional(),
|
|
166
229
|
aggregate: zod_1.z
|
|
167
230
|
.array(zod_1.z.object({
|
|
168
|
-
field:
|
|
231
|
+
field: selectFieldEnum,
|
|
169
232
|
fn: zod_1.z.enum(["sum", "avg", "min", "max", "count"]),
|
|
170
233
|
}))
|
|
171
|
-
.optional()
|
|
234
|
+
.optional()
|
|
235
|
+
.transform((val) => (val && val.length > 0 ? val : undefined)),
|
|
172
236
|
explain: zod_1.z.boolean().optional(),
|
|
173
237
|
})
|
|
174
238
|
.strict();
|
|
@@ -184,7 +248,8 @@ function registerQueryTool(resAnno, server, authEnabled) {
|
|
|
184
248
|
explain: inputZod.shape.explain,
|
|
185
249
|
};
|
|
186
250
|
const hint = resAnno.wrap?.hint ? ` Hint: ${resAnno.wrap?.hint}` : "";
|
|
187
|
-
const desc =
|
|
251
|
+
const desc = `${buildEnhancedQueryDescription(resAnno)} CRITICAL: Use foreign key fields (e.g., author_ID) for associations - association names (e.g., author) won't work in filters.` +
|
|
252
|
+
hint;
|
|
188
253
|
const queryHandler = async (rawArgs) => {
|
|
189
254
|
const parsed = inputZod.safeParse(rawArgs);
|
|
190
255
|
if (!parsed.success) {
|
|
@@ -203,7 +268,7 @@ function registerQueryTool(resAnno, server, authEnabled) {
|
|
|
203
268
|
}
|
|
204
269
|
let q;
|
|
205
270
|
try {
|
|
206
|
-
q = buildQuery(CDS, args, resAnno,
|
|
271
|
+
q = buildQuery(CDS, args, resAnno, allKeys);
|
|
207
272
|
}
|
|
208
273
|
catch (e) {
|
|
209
274
|
return (0, utils_2.toolError)("FILTER_PARSE_ERROR", e?.message || String(e));
|
|
@@ -564,8 +629,9 @@ function buildQuery(CDS, args, resAnno, propKeys) {
|
|
|
564
629
|
let qy = SELECT.from(resAnno.target).limit(limitTop, limitSkip);
|
|
565
630
|
if ((propKeys?.length ?? 0) === 0)
|
|
566
631
|
return qy;
|
|
567
|
-
if (args.select?.length)
|
|
632
|
+
if (args.select?.length) {
|
|
568
633
|
qy = qy.columns(...args.select);
|
|
634
|
+
}
|
|
569
635
|
if (args.orderby?.length) {
|
|
570
636
|
// Map to CQN-compatible order by fragments
|
|
571
637
|
const orderFragments = args.orderby.map((o) => `${o.field} ${o.dir}`);
|
|
@@ -575,29 +641,40 @@ function buildQuery(CDS, args, resAnno, propKeys) {
|
|
|
575
641
|
const ands = [];
|
|
576
642
|
if (args.q) {
|
|
577
643
|
const textFields = Array.from(resAnno.properties.keys()).filter((k) => /string/i.test(String(resAnno.properties.get(k))));
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
644
|
+
const escaped = String(args.q).replace(/'/g, "''");
|
|
645
|
+
const ors = textFields.map((f) => `contains(${f}, '${escaped}')`);
|
|
646
|
+
if (ors.length) {
|
|
647
|
+
const orExpr = ors.map((x) => `(${x})`).join(" or ");
|
|
648
|
+
ands.push(CDS.parse.expr(orExpr));
|
|
649
|
+
}
|
|
581
650
|
}
|
|
582
651
|
for (const c of args.where || []) {
|
|
583
652
|
const { field, op, value } = c;
|
|
653
|
+
// Field names are now consistent - use them directly
|
|
654
|
+
const actualField = field;
|
|
584
655
|
if (op === "in" && Array.isArray(value)) {
|
|
585
656
|
const list = value
|
|
586
657
|
.map((v) => typeof v === "string" ? `'${v.replace(/'/g, "''")}'` : String(v))
|
|
587
658
|
.join(",");
|
|
588
|
-
ands.push(CDS.parse.expr(`${
|
|
659
|
+
ands.push(CDS.parse.expr(`${actualField} in (${list})`));
|
|
589
660
|
continue;
|
|
590
661
|
}
|
|
591
662
|
const lit = typeof value === "string"
|
|
592
663
|
? `'${String(value).replace(/'/g, "''")}'`
|
|
593
664
|
: String(value);
|
|
665
|
+
// Map OData operators to CDS/SQL operators
|
|
666
|
+
const cdsOp = ODATA_TO_CDS_OPERATORS.get(op) ?? op;
|
|
594
667
|
const expr = ["contains", "startswith", "endswith"].includes(op)
|
|
595
|
-
? `${op}(${
|
|
596
|
-
: `${
|
|
668
|
+
? `${op}(${actualField}, ${lit})`
|
|
669
|
+
: `${actualField} ${cdsOp} ${lit}`;
|
|
597
670
|
ands.push(CDS.parse.expr(expr));
|
|
598
671
|
}
|
|
599
|
-
if (ands.length)
|
|
600
|
-
|
|
672
|
+
if (ands.length) {
|
|
673
|
+
// Apply each condition individually - CDS will AND them together
|
|
674
|
+
for (const condition of ands) {
|
|
675
|
+
qy = qy.where(condition);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
601
678
|
}
|
|
602
679
|
return qy;
|
|
603
680
|
}
|
package/lib/mcp/tools.js
CHANGED
|
@@ -6,6 +6,7 @@ const logger_1 = require("../logger");
|
|
|
6
6
|
const constants_1 = require("./constants");
|
|
7
7
|
const zod_1 = require("zod");
|
|
8
8
|
const utils_2 = require("../auth/utils");
|
|
9
|
+
const elicited_input_1 = require("./elicited-input");
|
|
9
10
|
/* @ts-ignore */
|
|
10
11
|
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
|
11
12
|
/**
|
|
@@ -37,7 +38,12 @@ function assignBoundOperation(params, model, server, authEnabled) {
|
|
|
37
38
|
throw new Error("Bound operation cannot be assigned to tool list, missing keys");
|
|
38
39
|
}
|
|
39
40
|
const keys = buildToolParameters(model.keyTypeMap);
|
|
40
|
-
const
|
|
41
|
+
const useElicitInput = (0, elicited_input_1.isElicitInput)(model.elicits);
|
|
42
|
+
const inputSchema = buildZodSchema({
|
|
43
|
+
...keys,
|
|
44
|
+
...(useElicitInput ? {} : params),
|
|
45
|
+
});
|
|
46
|
+
const elicitationRequests = (0, elicited_input_1.constructElicitationFunctions)(model, params);
|
|
41
47
|
server.registerTool(model.name, {
|
|
42
48
|
title: model.name,
|
|
43
49
|
description: model.description,
|
|
@@ -69,11 +75,15 @@ function assignBoundOperation(params, model, server, authEnabled) {
|
|
|
69
75
|
continue;
|
|
70
76
|
operationInput[k] = v;
|
|
71
77
|
}
|
|
78
|
+
const elicitationResult = await (0, elicited_input_1.handleElicitationRequests)(elicitationRequests, server);
|
|
79
|
+
if (elicitationResult?.earlyResponse) {
|
|
80
|
+
return elicitationResult.earlyResponse;
|
|
81
|
+
}
|
|
72
82
|
const accessRights = (0, utils_2.getAccessRights)(authEnabled);
|
|
73
83
|
const response = await service.tx({ user: accessRights }).send({
|
|
74
84
|
event: model.target,
|
|
75
85
|
entity: model.entityKey,
|
|
76
|
-
data: operationInput,
|
|
86
|
+
data: elicitationResult?.data ?? operationInput,
|
|
77
87
|
params: [operationKeys],
|
|
78
88
|
});
|
|
79
89
|
return (0, utils_1.asMcpResult)(response);
|
|
@@ -87,7 +97,9 @@ function assignBoundOperation(params, model, server, authEnabled) {
|
|
|
87
97
|
* @param server - MCP server instance to register with
|
|
88
98
|
*/
|
|
89
99
|
function assignUnboundOperation(params, model, server, authEnabled) {
|
|
90
|
-
const
|
|
100
|
+
const useElicitInput = (0, elicited_input_1.isElicitInput)(model.elicits);
|
|
101
|
+
const inputSchema = buildZodSchema(useElicitInput ? {} : params);
|
|
102
|
+
const elicitationRequests = (0, elicited_input_1.constructElicitationFunctions)(model, params);
|
|
91
103
|
server.registerTool(model.name, {
|
|
92
104
|
title: model.name,
|
|
93
105
|
description: model.description,
|
|
@@ -109,10 +121,14 @@ function assignUnboundOperation(params, model, server, authEnabled) {
|
|
|
109
121
|
],
|
|
110
122
|
};
|
|
111
123
|
}
|
|
124
|
+
const elicitationResult = await (0, elicited_input_1.handleElicitationRequests)(elicitationRequests, server);
|
|
125
|
+
if (elicitationResult?.earlyResponse) {
|
|
126
|
+
return elicitationResult.earlyResponse;
|
|
127
|
+
}
|
|
112
128
|
const accessRights = (0, utils_2.getAccessRights)(authEnabled);
|
|
113
129
|
const response = await service
|
|
114
130
|
.tx({ user: accessRights })
|
|
115
|
-
.send(model.target, args);
|
|
131
|
+
.send(model.target, elicitationResult?.data ?? args);
|
|
116
132
|
return (0, utils_1.asMcpResult)(response);
|
|
117
133
|
});
|
|
118
134
|
}
|
|
@@ -130,27 +146,6 @@ function buildToolParameters(params) {
|
|
|
130
146
|
}
|
|
131
147
|
return result;
|
|
132
148
|
}
|
|
133
|
-
/**
|
|
134
|
-
* Converts a value to a string representation suitable for MCP responses
|
|
135
|
-
* Handles objects and arrays by JSON stringifying them instead of using String()
|
|
136
|
-
* @param value - The value to convert to string
|
|
137
|
-
* @returns String representation of the value
|
|
138
|
-
*/
|
|
139
|
-
function formatResponseValue(value) {
|
|
140
|
-
if (value === null || value === undefined) {
|
|
141
|
-
return String(value);
|
|
142
|
-
}
|
|
143
|
-
if (typeof value === "object") {
|
|
144
|
-
try {
|
|
145
|
-
return JSON.stringify(value, null, 2);
|
|
146
|
-
}
|
|
147
|
-
catch (error) {
|
|
148
|
-
// Fallback to String() if JSON.stringify fails (e.g., circular references)
|
|
149
|
-
return String(value);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return String(value);
|
|
153
|
-
}
|
|
154
149
|
/**
|
|
155
150
|
* Constructs a complete Zod schema object for MCP tool input validation
|
|
156
151
|
* @param params - Record of parameter names to Zod schema types
|
package/lib/mcp/utils.js
CHANGED
package/lib/mcp.js
CHANGED
|
@@ -40,9 +40,7 @@ class McpPlugin {
|
|
|
40
40
|
logger_1.LOGGER.debug("Event received for 'bootstrap'");
|
|
41
41
|
this.expressApp = app;
|
|
42
42
|
this.expressApp.use("/mcp", express_1.default.json());
|
|
43
|
-
|
|
44
|
-
// Users will have to explicitly write none
|
|
45
|
-
if (this.config.auth !== "none") {
|
|
43
|
+
if (this.config.auth === "inherit") {
|
|
46
44
|
(0, utils_2.registerAuthMiddleware)(this.expressApp);
|
|
47
45
|
}
|
|
48
46
|
await this.registerApiEndpoints();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gavdi/cap-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "MCP Pluging for CAP",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"MCP",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"express": "^4"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@modelcontextprotocol/sdk": "^1.17.
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.17.3",
|
|
45
45
|
"zod": "^3.25.67",
|
|
46
46
|
"zod-to-json-schema": "^3.24.5"
|
|
47
47
|
},
|
package/lib/.DS_Store
DELETED
|
Binary file
|
package/lib/annotations.js
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.McpPromptAnnotation = exports.McpToolAnnotation = exports.McpResourceAnnotation = exports.McpAnnotation = exports.McpAnnotations = exports.McpAnnotationKey = void 0;
|
|
4
|
-
exports.parseAnnotations = parseAnnotations;
|
|
5
|
-
const utils_1 = require("./utils");
|
|
6
|
-
const DEFAULT_ALL_RESOURCE_OPTIONS = new Set([
|
|
7
|
-
"filter",
|
|
8
|
-
"sort",
|
|
9
|
-
"top",
|
|
10
|
-
"skip",
|
|
11
|
-
"select",
|
|
12
|
-
]);
|
|
13
|
-
exports.McpAnnotationKey = "@mcp";
|
|
14
|
-
exports.McpAnnotations = {
|
|
15
|
-
// Resource annotations for MCP
|
|
16
|
-
MCP_RESOURCE: "@mcp.resource",
|
|
17
|
-
// Tool annotations for MCP
|
|
18
|
-
MCP_TOOL_NAME: "@mcp.tool.name",
|
|
19
|
-
MCP_TOOL_DESCRIPTION: "@mcp.tool.description",
|
|
20
|
-
// Prompt annotations for MCP
|
|
21
|
-
MCP_PROMPT: "@mcp.prompt",
|
|
22
|
-
};
|
|
23
|
-
class McpAnnotation {
|
|
24
|
-
_target;
|
|
25
|
-
_serviceName;
|
|
26
|
-
constructor(target, serviceName) {
|
|
27
|
-
this._target = target;
|
|
28
|
-
this._serviceName = serviceName;
|
|
29
|
-
}
|
|
30
|
-
get target() {
|
|
31
|
-
return this._target;
|
|
32
|
-
}
|
|
33
|
-
get serviceName() {
|
|
34
|
-
return this._serviceName;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
exports.McpAnnotation = McpAnnotation;
|
|
38
|
-
class McpResourceAnnotation extends McpAnnotation {
|
|
39
|
-
_includeAll;
|
|
40
|
-
_functionalities;
|
|
41
|
-
_properties;
|
|
42
|
-
constructor(target, serviceName, includeAll, functionalities, properties) {
|
|
43
|
-
super(target, serviceName);
|
|
44
|
-
this._includeAll = includeAll;
|
|
45
|
-
this._functionalities = functionalities;
|
|
46
|
-
this._properties = properties;
|
|
47
|
-
}
|
|
48
|
-
get includeAll() {
|
|
49
|
-
return this._includeAll;
|
|
50
|
-
}
|
|
51
|
-
get functionalities() {
|
|
52
|
-
return this._functionalities;
|
|
53
|
-
}
|
|
54
|
-
get properties() {
|
|
55
|
-
return this._properties;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
exports.McpResourceAnnotation = McpResourceAnnotation;
|
|
59
|
-
class McpToolAnnotation extends McpAnnotation {
|
|
60
|
-
_name;
|
|
61
|
-
_description;
|
|
62
|
-
_parameters;
|
|
63
|
-
_entityKey;
|
|
64
|
-
_operationKind;
|
|
65
|
-
_keyTypeMap;
|
|
66
|
-
constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap) {
|
|
67
|
-
super(operation, serviceName);
|
|
68
|
-
this._name = name;
|
|
69
|
-
this._description = description;
|
|
70
|
-
this._parameters = parameters;
|
|
71
|
-
this._entityKey = entityKey;
|
|
72
|
-
this._operationKind = operationKind;
|
|
73
|
-
this._keyTypeMap = keyTypeMap;
|
|
74
|
-
}
|
|
75
|
-
get name() {
|
|
76
|
-
return this._name;
|
|
77
|
-
}
|
|
78
|
-
get description() {
|
|
79
|
-
return this._description;
|
|
80
|
-
}
|
|
81
|
-
get parameters() {
|
|
82
|
-
return this._parameters;
|
|
83
|
-
}
|
|
84
|
-
get entityKey() {
|
|
85
|
-
return this._entityKey;
|
|
86
|
-
}
|
|
87
|
-
get operationKind() {
|
|
88
|
-
return this._operationKind;
|
|
89
|
-
}
|
|
90
|
-
get keyTypeMap() {
|
|
91
|
-
return this._keyTypeMap;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
exports.McpToolAnnotation = McpToolAnnotation;
|
|
95
|
-
class McpPromptAnnotation extends McpAnnotation {
|
|
96
|
-
_name;
|
|
97
|
-
_template;
|
|
98
|
-
constructor(target, serviceName, name, template) {
|
|
99
|
-
super(target, serviceName);
|
|
100
|
-
this._name = name;
|
|
101
|
-
this._template = template;
|
|
102
|
-
}
|
|
103
|
-
get name() {
|
|
104
|
-
return this._name;
|
|
105
|
-
}
|
|
106
|
-
get template() {
|
|
107
|
-
return this._template;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
exports.McpPromptAnnotation = McpPromptAnnotation;
|
|
111
|
-
function parseAnnotations(services) {
|
|
112
|
-
const annotations = [];
|
|
113
|
-
for (const serviceName of Object.keys(services)) {
|
|
114
|
-
const srv = services[serviceName];
|
|
115
|
-
if (srv.name === "CatalogService") {
|
|
116
|
-
utils_1.LOGGER.debug("SERVICE: ", srv.model.definitions);
|
|
117
|
-
}
|
|
118
|
-
const entities = srv.entities;
|
|
119
|
-
const operations = srv.operations; // Refers to action and function imports
|
|
120
|
-
// Find entities
|
|
121
|
-
for (const entityName of Object.keys(entities)) {
|
|
122
|
-
const target = entities[entityName];
|
|
123
|
-
const res = findEntityAnnotations(target, entityName, srv);
|
|
124
|
-
if (target.actions) {
|
|
125
|
-
const bound = parseBoundOperations(target.actions, entityName, target, srv);
|
|
126
|
-
if (bound && bound.length > 0) {
|
|
127
|
-
annotations.push(...bound);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (!res)
|
|
131
|
-
continue;
|
|
132
|
-
annotations.push(res);
|
|
133
|
-
}
|
|
134
|
-
// Find operations
|
|
135
|
-
for (const operationName of Object.keys(operations)) {
|
|
136
|
-
const op = operations[operationName];
|
|
137
|
-
const res = findOperationAnnotations(op, operationName, srv);
|
|
138
|
-
if (!res)
|
|
139
|
-
continue;
|
|
140
|
-
annotations.push(res);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const result = formatAnnotations(annotations);
|
|
144
|
-
return result;
|
|
145
|
-
}
|
|
146
|
-
function formatAnnotations(annotationList) {
|
|
147
|
-
const result = new Map();
|
|
148
|
-
for (const annotation of annotationList) {
|
|
149
|
-
if (annotation.operation) {
|
|
150
|
-
if (!annotation.annotations[exports.McpAnnotations.MCP_TOOL_NAME] ||
|
|
151
|
-
!annotation.annotations[exports.McpAnnotations.MCP_TOOL_DESCRIPTION]) {
|
|
152
|
-
utils_1.LOGGER.error(`Invalid annotation found for operation`, annotation);
|
|
153
|
-
throw new Error(`Invalid annotations for operation '${annotation.operation}'`);
|
|
154
|
-
}
|
|
155
|
-
else if (typeof annotation.annotations[exports.McpAnnotations.MCP_TOOL_NAME] !==
|
|
156
|
-
"string" ||
|
|
157
|
-
typeof annotation.annotations[exports.McpAnnotations.MCP_TOOL_DESCRIPTION] !==
|
|
158
|
-
"string") {
|
|
159
|
-
utils_1.LOGGER.error("Invalid data for annotations", annotation);
|
|
160
|
-
throw new Error(`Invalid annotation data for operation '${annotation.operation}'`);
|
|
161
|
-
}
|
|
162
|
-
const entry = new McpToolAnnotation(annotation.annotations[exports.McpAnnotations.MCP_TOOL_NAME], annotation.annotations[exports.McpAnnotations.MCP_TOOL_DESCRIPTION], annotation.operation, annotation.serviceName, mapOperationInput(annotation.context), // TODO: Parse the parameters from the context and place them in the class
|
|
163
|
-
annotation.entityKey, annotation.operationKind, annotation.keyTypeMap);
|
|
164
|
-
result.set(entry.target, entry);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (!annotation.entityKey) {
|
|
168
|
-
utils_1.LOGGER.error("Invalid entry", annotation);
|
|
169
|
-
throw new Error(`Invalid annotated entry found with no target`);
|
|
170
|
-
}
|
|
171
|
-
if (!annotation.annotations[exports.McpAnnotations.MCP_RESOURCE]) {
|
|
172
|
-
utils_1.LOGGER.error("No valid annotations found for entry", annotation);
|
|
173
|
-
throw new Error(`Invalid annotations for entry target: '${annotation.entityKey}'`);
|
|
174
|
-
}
|
|
175
|
-
const includeAll = annotation.annotations[exports.McpAnnotations.MCP_RESOURCE] === true;
|
|
176
|
-
const functionalities = Array.isArray(annotation.annotations[exports.McpAnnotations.MCP_RESOURCE])
|
|
177
|
-
? new Set(annotation.annotations[exports.McpAnnotations.MCP_RESOURCE])
|
|
178
|
-
: DEFAULT_ALL_RESOURCE_OPTIONS;
|
|
179
|
-
const entry = new McpResourceAnnotation(annotation.entityKey, annotation.serviceName, includeAll, functionalities, (0, utils_1.parseEntityElements)(annotation.context));
|
|
180
|
-
result.set(entry.target, entry);
|
|
181
|
-
}
|
|
182
|
-
utils_1.LOGGER.debug("Formatted annotations", result);
|
|
183
|
-
return result;
|
|
184
|
-
}
|
|
185
|
-
function findEntityAnnotations(entry, entityKey, service) {
|
|
186
|
-
const annotations = findAnnotations(entry);
|
|
187
|
-
return Object.keys(annotations).length > 0
|
|
188
|
-
? {
|
|
189
|
-
serviceName: service.name,
|
|
190
|
-
annotations: annotations,
|
|
191
|
-
entityKey: entityKey,
|
|
192
|
-
context: entry,
|
|
193
|
-
}
|
|
194
|
-
: undefined;
|
|
195
|
-
}
|
|
196
|
-
function findOperationAnnotations(operation, operationName, service) {
|
|
197
|
-
const annotations = findAnnotations(operation);
|
|
198
|
-
return Object.keys(annotations).length > 0
|
|
199
|
-
? {
|
|
200
|
-
serviceName: service.name,
|
|
201
|
-
annotations: annotations,
|
|
202
|
-
operation: operationName,
|
|
203
|
-
operationKind: operation.kind,
|
|
204
|
-
context: operation,
|
|
205
|
-
}
|
|
206
|
-
: undefined;
|
|
207
|
-
}
|
|
208
|
-
function parseBoundOperations(operations, entityKey, entity, service) {
|
|
209
|
-
const res = new Array();
|
|
210
|
-
for (const [operationName, operation] of Object.entries(operations)) {
|
|
211
|
-
const annotation = findBoundOperationAnnotations(operation, operationName, entityKey, service);
|
|
212
|
-
if (!annotation)
|
|
213
|
-
continue;
|
|
214
|
-
annotation.keyTypeMap = new Map();
|
|
215
|
-
for (const [k, v] of Object.entries(entity.keys)) {
|
|
216
|
-
if (!v.type) {
|
|
217
|
-
utils_1.LOGGER.error("Invalid key type", k);
|
|
218
|
-
throw new Error("Invalid key type found for bound operation");
|
|
219
|
-
}
|
|
220
|
-
annotation.keyTypeMap.set(k, v.type.replace("cds.", ""));
|
|
221
|
-
}
|
|
222
|
-
res.push(annotation);
|
|
223
|
-
}
|
|
224
|
-
return res;
|
|
225
|
-
}
|
|
226
|
-
function findBoundOperationAnnotations(operation, operationName, entityKey, service) {
|
|
227
|
-
const annotations = findAnnotations(operation);
|
|
228
|
-
return Object.keys(annotations).length > 0
|
|
229
|
-
? {
|
|
230
|
-
serviceName: service.name,
|
|
231
|
-
annotations: annotations,
|
|
232
|
-
operation: operationName,
|
|
233
|
-
operationKind: operation.kind,
|
|
234
|
-
entityKey: entityKey,
|
|
235
|
-
context: operation,
|
|
236
|
-
}
|
|
237
|
-
: undefined;
|
|
238
|
-
}
|
|
239
|
-
function findAnnotations(entry) {
|
|
240
|
-
const annotations = {};
|
|
241
|
-
for (const [k, v] of Object.entries(entry)) {
|
|
242
|
-
if (!k.includes(exports.McpAnnotationKey))
|
|
243
|
-
continue;
|
|
244
|
-
annotations[k] = v;
|
|
245
|
-
}
|
|
246
|
-
return annotations;
|
|
247
|
-
}
|
|
248
|
-
function mapOperationInput(ctx) {
|
|
249
|
-
const params = ctx["params"];
|
|
250
|
-
if (!params)
|
|
251
|
-
return undefined;
|
|
252
|
-
const result = new Map();
|
|
253
|
-
for (const [k, v] of Object.entries(params)) {
|
|
254
|
-
result.set(k, v.type.replace("cds.", ""));
|
|
255
|
-
}
|
|
256
|
-
return result.size > 0 ? result : undefined;
|
|
257
|
-
}
|
package/lib/auth/adapter.js
DELETED
package/lib/auth/mock.js
DELETED
package/lib/auth/types.js
DELETED