@gavdi/cap-mcp 0.9.7 → 0.9.8
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 -1
- package/lib/annotations/constants.js +9 -1
- package/lib/annotations/parser.js +15 -3
- package/lib/annotations/structures.js +20 -6
- package/lib/annotations/utils.js +62 -0
- package/lib/auth/utils.js +60 -0
- package/lib/config/loader.js +1 -0
- package/lib/mcp/entity-tools.js +86 -6
- package/lib/mcp/factory.js +9 -3
- package/lib/mcp.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,7 +62,8 @@ Add MCP configuration to your `package.json`:
|
|
|
62
62
|
"name": "my-bookshop-mcp",
|
|
63
63
|
"auth": "inherit",
|
|
64
64
|
"wrap_entities_to_actions": false,
|
|
65
|
-
"wrap_entity_modes": ["query", "get"]
|
|
65
|
+
"wrap_entity_modes": ["query", "get"],
|
|
66
|
+
"instructions": "MCP server instructions for agents"
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
}
|
|
@@ -251,6 +252,7 @@ Configure the MCP plugin through your CAP application's `package.json` or `.cdsr
|
|
|
251
252
|
"name": "my-mcp-server",
|
|
252
253
|
"version": "1.0.0",
|
|
253
254
|
"auth": "inherit",
|
|
255
|
+
"instructions": "mcp server instructions for agents",
|
|
254
256
|
"capabilities": {
|
|
255
257
|
"resources": {
|
|
256
258
|
"listChanged": true,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_ALL_RESOURCE_OPTIONS = exports.MCP_ANNOTATION_PROPS = exports.MCP_ANNOTATION_KEY = void 0;
|
|
3
|
+
exports.DEFAULT_ALL_RESOURCE_OPTIONS = exports.CDS_AUTH_ANNOTATIONS = exports.MCP_ANNOTATION_PROPS = 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
|
|
@@ -28,6 +28,14 @@ exports.MCP_ANNOTATION_PROPS = {
|
|
|
28
28
|
/** Wrapper configuration for exposing entities as tools */
|
|
29
29
|
MCP_WRAP: "@mcp.wrap",
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Set of annotations used for CDS auth annotations
|
|
33
|
+
* Maps logical names to their actual annotation keys used in CDS files.
|
|
34
|
+
*/
|
|
35
|
+
exports.CDS_AUTH_ANNOTATIONS = {
|
|
36
|
+
REQUIRES: "@requires",
|
|
37
|
+
RESTRICT: "@restrict",
|
|
38
|
+
};
|
|
31
39
|
/**
|
|
32
40
|
* Default set of all available OData query options for MCP resources
|
|
33
41
|
* Used when @mcp.resource is set to `true` to enable all capabilities
|
|
@@ -70,8 +70,12 @@ function parseAnnotations(definition) {
|
|
|
70
70
|
definition: definition,
|
|
71
71
|
};
|
|
72
72
|
for (const [k, v] of Object.entries(definition)) {
|
|
73
|
-
|
|
73
|
+
// Process MCP annotations and CDS auth annotations
|
|
74
|
+
if (!k.includes(constants_1.MCP_ANNOTATION_KEY) &&
|
|
75
|
+
!k.startsWith("@requires") &&
|
|
76
|
+
!k.startsWith("@restrict")) {
|
|
74
77
|
continue;
|
|
78
|
+
}
|
|
75
79
|
logger_1.LOGGER.debug("Parsing: ", k, v);
|
|
76
80
|
switch (k) {
|
|
77
81
|
case constants_1.MCP_ANNOTATION_PROPS.MCP_NAME:
|
|
@@ -93,6 +97,12 @@ function parseAnnotations(definition) {
|
|
|
93
97
|
// Wrapper container to expose resources as tools
|
|
94
98
|
annotations.wrap = v;
|
|
95
99
|
continue;
|
|
100
|
+
case constants_1.CDS_AUTH_ANNOTATIONS.REQUIRES:
|
|
101
|
+
annotations.requires = v;
|
|
102
|
+
continue;
|
|
103
|
+
case constants_1.CDS_AUTH_ANNOTATIONS.RESTRICT:
|
|
104
|
+
annotations.restrict = v;
|
|
105
|
+
continue;
|
|
96
106
|
default:
|
|
97
107
|
continue;
|
|
98
108
|
}
|
|
@@ -112,7 +122,8 @@ function constructResourceAnnotation(serviceName, target, annotations, definitio
|
|
|
112
122
|
return undefined;
|
|
113
123
|
const functionalities = (0, utils_1.determineResourceOptions)(annotations);
|
|
114
124
|
const { properties, resourceKeys } = (0, utils_1.parseResourceElements)(definition);
|
|
115
|
-
|
|
125
|
+
const restrictions = (0, utils_1.parseCdsRestrictions)(annotations.restrict, annotations.requires);
|
|
126
|
+
return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys, annotations.wrap, restrictions);
|
|
116
127
|
}
|
|
117
128
|
/**
|
|
118
129
|
* Constructs a tool annotation from parsed annotation data
|
|
@@ -127,7 +138,8 @@ function constructToolAnnotation(serviceName, target, annotations, entityKey, ke
|
|
|
127
138
|
if (!(0, utils_1.isValidToolAnnotation)(annotations))
|
|
128
139
|
return undefined;
|
|
129
140
|
const { parameters, operationKind } = (0, utils_1.parseOperationElements)(annotations);
|
|
130
|
-
|
|
141
|
+
const restrictions = (0, utils_1.parseCdsRestrictions)(annotations.restrict, annotations.requires);
|
|
142
|
+
return new structures_1.McpToolAnnotation(annotations.name, annotations.description, target, serviceName, parameters, entityKey, operationKind, keyParams, restrictions);
|
|
131
143
|
}
|
|
132
144
|
/**
|
|
133
145
|
* Constructs a prompt annotation from parsed annotation data
|
|
@@ -14,18 +14,22 @@ class McpAnnotation {
|
|
|
14
14
|
_target;
|
|
15
15
|
/** The name of the CAP service this annotation belongs to */
|
|
16
16
|
_serviceName;
|
|
17
|
+
/** Auth roles by providing CDS that is required for use */
|
|
18
|
+
_restrictions;
|
|
17
19
|
/**
|
|
18
20
|
* Creates a new MCP annotation instance
|
|
19
21
|
* @param name - Unique identifier for this annotation
|
|
20
22
|
* @param description - Human-readable description
|
|
21
23
|
* @param target - The target element this annotation applies to
|
|
22
24
|
* @param serviceName - Name of the associated CAP service
|
|
25
|
+
* @param restrictions - Roles required for the given annotation
|
|
23
26
|
*/
|
|
24
|
-
constructor(name, description, target, serviceName) {
|
|
27
|
+
constructor(name, description, target, serviceName, restrictions) {
|
|
25
28
|
this._name = name;
|
|
26
29
|
this._description = description;
|
|
27
30
|
this._target = target;
|
|
28
31
|
this._serviceName = serviceName;
|
|
32
|
+
this._restrictions = restrictions;
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
31
35
|
* Gets the unique name identifier for this annotation
|
|
@@ -55,6 +59,14 @@ class McpAnnotation {
|
|
|
55
59
|
get serviceName() {
|
|
56
60
|
return this._serviceName;
|
|
57
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Gets the list of roles required for access to the annotation.
|
|
64
|
+
* If the list is empty, then all can access.
|
|
65
|
+
* @returns List of required roles
|
|
66
|
+
*/
|
|
67
|
+
get restrictions() {
|
|
68
|
+
return this._restrictions;
|
|
69
|
+
}
|
|
58
70
|
}
|
|
59
71
|
exports.McpAnnotation = McpAnnotation;
|
|
60
72
|
/**
|
|
@@ -79,9 +91,10 @@ class McpResourceAnnotation extends McpAnnotation {
|
|
|
79
91
|
* @param functionalities - Set of enabled OData query options (filter, top, skip, etc.)
|
|
80
92
|
* @param properties - Map of entity properties to their CDS types
|
|
81
93
|
* @param resourceKeys - Map of key fields to their types
|
|
94
|
+
* @param restrictions - Optional restrictions based on CDS roles
|
|
82
95
|
*/
|
|
83
|
-
constructor(name, description, target, serviceName, functionalities, properties, resourceKeys, wrap) {
|
|
84
|
-
super(name, description, target, serviceName);
|
|
96
|
+
constructor(name, description, target, serviceName, functionalities, properties, resourceKeys, wrap, restrictions) {
|
|
97
|
+
super(name, description, target, serviceName, restrictions ?? []);
|
|
85
98
|
this._functionalities = functionalities;
|
|
86
99
|
this._properties = properties;
|
|
87
100
|
this._resourceKeys = resourceKeys;
|
|
@@ -139,9 +152,10 @@ class McpToolAnnotation extends McpAnnotation {
|
|
|
139
152
|
* @param entityKey - Optional entity key field for bound operations
|
|
140
153
|
* @param operationKind - Optional operation type ('function' or 'action')
|
|
141
154
|
* @param keyTypeMap - Optional map of key fields to types for bound operations
|
|
155
|
+
* @param restrictions - Optional restrictions based on CDS roles
|
|
142
156
|
*/
|
|
143
|
-
constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap) {
|
|
144
|
-
super(name, description, operation, serviceName);
|
|
157
|
+
constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap, restrictions) {
|
|
158
|
+
super(name, description, operation, serviceName, restrictions ?? []);
|
|
145
159
|
this._parameters = parameters;
|
|
146
160
|
this._entityKey = entityKey;
|
|
147
161
|
this._operationKind = operationKind;
|
|
@@ -192,7 +206,7 @@ class McpPromptAnnotation extends McpAnnotation {
|
|
|
192
206
|
* @param prompts - Array of prompt template definitions
|
|
193
207
|
*/
|
|
194
208
|
constructor(name, description, serviceName, prompts) {
|
|
195
|
-
super(name, description, serviceName, serviceName);
|
|
209
|
+
super(name, description, serviceName, serviceName, []);
|
|
196
210
|
this._prompts = prompts;
|
|
197
211
|
}
|
|
198
212
|
/**
|
package/lib/annotations/utils.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.determineResourceOptions = determineResourceOptions;
|
|
|
10
10
|
exports.parseResourceElements = parseResourceElements;
|
|
11
11
|
exports.parseOperationElements = parseOperationElements;
|
|
12
12
|
exports.parseEntityKeys = parseEntityKeys;
|
|
13
|
+
exports.parseCdsRestrictions = parseCdsRestrictions;
|
|
13
14
|
const constants_1 = require("./constants");
|
|
14
15
|
const logger_1 = require("../logger");
|
|
15
16
|
/**
|
|
@@ -191,3 +192,64 @@ function parseEntityKeys(definition) {
|
|
|
191
192
|
}
|
|
192
193
|
return result;
|
|
193
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Parses the CDS role restrictions to be used for MCP
|
|
197
|
+
*/
|
|
198
|
+
function parseCdsRestrictions(restrictions, requires) {
|
|
199
|
+
if (!restrictions && !requires)
|
|
200
|
+
return [];
|
|
201
|
+
const result = [];
|
|
202
|
+
if (requires) {
|
|
203
|
+
result.push({
|
|
204
|
+
role: requires,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (!restrictions || restrictions.length <= 0)
|
|
208
|
+
return result;
|
|
209
|
+
for (const el of restrictions) {
|
|
210
|
+
const ops = mapOperationRestriction(el.grant);
|
|
211
|
+
if (!el.to) {
|
|
212
|
+
result.push({
|
|
213
|
+
role: "authenticated-user",
|
|
214
|
+
operations: ops,
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const mapped = el.to.map((to) => ({
|
|
219
|
+
role: to,
|
|
220
|
+
operations: ops,
|
|
221
|
+
}));
|
|
222
|
+
result.push(...mapped);
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Maps the "grant" property from CdsRestriction to McpRestriction
|
|
228
|
+
*/
|
|
229
|
+
function mapOperationRestriction(cdsRestrictions) {
|
|
230
|
+
const result = [];
|
|
231
|
+
if (!cdsRestrictions || cdsRestrictions.length <= 0) {
|
|
232
|
+
result.push("CREATE");
|
|
233
|
+
result.push("READ");
|
|
234
|
+
result.push("UPDATE");
|
|
235
|
+
result.push("DELETE");
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
for (const el of cdsRestrictions) {
|
|
239
|
+
switch (el) {
|
|
240
|
+
case "CHANGE":
|
|
241
|
+
result.push("UPDATE");
|
|
242
|
+
continue;
|
|
243
|
+
case "*":
|
|
244
|
+
result.push("CREATE");
|
|
245
|
+
result.push("READ");
|
|
246
|
+
result.push("UPDATE");
|
|
247
|
+
result.push("DELETE");
|
|
248
|
+
continue;
|
|
249
|
+
default:
|
|
250
|
+
result.push(el);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
package/lib/auth/utils.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isAuthEnabled = isAuthEnabled;
|
|
4
4
|
exports.getAccessRights = getAccessRights;
|
|
5
5
|
exports.registerAuthMiddleware = registerAuthMiddleware;
|
|
6
|
+
exports.hasToolOperationAccess = hasToolOperationAccess;
|
|
7
|
+
exports.getWrapAccesses = getWrapAccesses;
|
|
6
8
|
const handler_1 = require("./handler");
|
|
7
9
|
const proxyProvider_js_1 = require("@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js");
|
|
8
10
|
const router_js_1 = require("@modelcontextprotocol/sdk/server/auth/router.js");
|
|
@@ -210,3 +212,61 @@ function configureOAuthProxy(expressApp) {
|
|
|
210
212
|
serviceDocumentationUrl: new URL("https://docs.cloudfoundry.org/api/uaa/version/77.34.0/index.html#authorization"),
|
|
211
213
|
}));
|
|
212
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Checks whether the requesting user's access matches that of the roles required
|
|
217
|
+
* @param user
|
|
218
|
+
* @returns true if the user has access
|
|
219
|
+
*/
|
|
220
|
+
function hasToolOperationAccess(user, roles) {
|
|
221
|
+
// If no restrictions are defined, allow access
|
|
222
|
+
if (!roles || roles.length === 0)
|
|
223
|
+
return true;
|
|
224
|
+
for (const el of roles) {
|
|
225
|
+
if (user.is(el.role))
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Determines wrap accesses based on the given MCP restrictions derived from annotations
|
|
232
|
+
* @param user
|
|
233
|
+
* @param restrictions
|
|
234
|
+
* @returns wrap tool accesses
|
|
235
|
+
*/
|
|
236
|
+
function getWrapAccesses(user, restrictions) {
|
|
237
|
+
// If no restrictions are defined, allow all access
|
|
238
|
+
if (!restrictions || restrictions.length === 0) {
|
|
239
|
+
return {
|
|
240
|
+
canRead: true,
|
|
241
|
+
canCreate: true,
|
|
242
|
+
canUpdate: true,
|
|
243
|
+
canDelete: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const access = {};
|
|
247
|
+
for (const el of restrictions) {
|
|
248
|
+
// If the user does not even have the role then no reason to check
|
|
249
|
+
if (!user.is(el.role))
|
|
250
|
+
continue;
|
|
251
|
+
if (!el.operations || el.operations.length <= 0) {
|
|
252
|
+
access.canRead = true;
|
|
253
|
+
access.canCreate = true;
|
|
254
|
+
access.canDelete = true;
|
|
255
|
+
access.canUpdate = true;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
if (el.operations.includes("READ")) {
|
|
259
|
+
access.canRead = true;
|
|
260
|
+
}
|
|
261
|
+
if (el.operations.includes("UPDATE")) {
|
|
262
|
+
access.canUpdate = true;
|
|
263
|
+
}
|
|
264
|
+
if (el.operations.includes("CREATE")) {
|
|
265
|
+
access.canCreate = true;
|
|
266
|
+
}
|
|
267
|
+
if (el.operations.includes("DELETE")) {
|
|
268
|
+
access.canDelete = true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return access;
|
|
272
|
+
}
|
package/lib/config/loader.js
CHANGED
package/lib/mcp/entity-tools.js
CHANGED
|
@@ -66,28 +66,36 @@ const TIMEOUT_MS = 10_000; // Standard timeout for tool calls (ms)
|
|
|
66
66
|
* Modes can be controlled globally via configuration and per-entity via @mcp.wrap.
|
|
67
67
|
*
|
|
68
68
|
* Example tool names (naming is explicit for easier LLM usage):
|
|
69
|
-
* Service_Entity_query, Service_Entity_get, Service_Entity_create, Service_Entity_update
|
|
69
|
+
* Service_Entity_query, Service_Entity_get, Service_Entity_create, Service_Entity_update, Service_Entity_delete
|
|
70
70
|
*/
|
|
71
|
-
function registerEntityWrappers(resAnno, server, authEnabled, defaultModes) {
|
|
71
|
+
function registerEntityWrappers(resAnno, server, authEnabled, defaultModes, accesses) {
|
|
72
72
|
const CDS = global.cds;
|
|
73
73
|
logger_1.LOGGER.debug(`[REGISTRATION TIME] Registering entity wrappers for ${resAnno.serviceName}.${resAnno.target}, available services:`, Object.keys(CDS.services || {}));
|
|
74
74
|
const modes = resAnno.wrap?.modes ?? defaultModes;
|
|
75
|
-
if (modes.includes("query")) {
|
|
75
|
+
if (modes.includes("query") && accesses.canRead) {
|
|
76
76
|
registerQueryTool(resAnno, server, authEnabled);
|
|
77
77
|
}
|
|
78
78
|
if (modes.includes("get") &&
|
|
79
79
|
resAnno.resourceKeys &&
|
|
80
|
-
resAnno.resourceKeys.size > 0
|
|
80
|
+
resAnno.resourceKeys.size > 0 &&
|
|
81
|
+
accesses.canRead) {
|
|
81
82
|
registerGetTool(resAnno, server, authEnabled);
|
|
82
83
|
}
|
|
83
|
-
if (modes.includes("create")) {
|
|
84
|
+
if (modes.includes("create") && accesses.canCreate) {
|
|
84
85
|
registerCreateTool(resAnno, server, authEnabled);
|
|
85
86
|
}
|
|
86
87
|
if (modes.includes("update") &&
|
|
87
88
|
resAnno.resourceKeys &&
|
|
88
|
-
resAnno.resourceKeys.size > 0
|
|
89
|
+
resAnno.resourceKeys.size > 0 &&
|
|
90
|
+
accesses.canUpdate) {
|
|
89
91
|
registerUpdateTool(resAnno, server, authEnabled);
|
|
90
92
|
}
|
|
93
|
+
if (modes.includes("delete") &&
|
|
94
|
+
resAnno.resourceKeys &&
|
|
95
|
+
resAnno.resourceKeys.size > 0 &&
|
|
96
|
+
accesses.canDelete) {
|
|
97
|
+
registerDeleteTool(resAnno, server, authEnabled);
|
|
98
|
+
}
|
|
91
99
|
}
|
|
92
100
|
/**
|
|
93
101
|
* Builds the visible tool name for a given operation mode.
|
|
@@ -474,6 +482,78 @@ function registerUpdateTool(resAnno, server, authEnabled) {
|
|
|
474
482
|
};
|
|
475
483
|
server.registerTool(toolName, { title: toolName, description: desc, inputSchema }, updateHandler);
|
|
476
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Registers the delete tool for an entity.
|
|
487
|
+
* Requires keys to identify the entity to delete.
|
|
488
|
+
*/
|
|
489
|
+
function registerDeleteTool(resAnno, server, authEnabled) {
|
|
490
|
+
const toolName = nameFor(resAnno.serviceName, resAnno.target, "delete");
|
|
491
|
+
const inputSchema = {};
|
|
492
|
+
// Keys required for deletion
|
|
493
|
+
for (const [k, cdsType] of resAnno.resourceKeys.entries()) {
|
|
494
|
+
inputSchema[k] = (0, utils_2.determineMcpParameterType)(cdsType).describe(`Key ${k}`);
|
|
495
|
+
}
|
|
496
|
+
const keyList = Array.from(resAnno.resourceKeys.keys()).join(", ");
|
|
497
|
+
const hint = resAnno.wrap?.hint ? ` Hint: ${resAnno.wrap?.hint}` : "";
|
|
498
|
+
const desc = `Delete ${resAnno.target} by key(s): ${keyList}. This operation cannot be undone.${hint}`;
|
|
499
|
+
const deleteHandler = async (args) => {
|
|
500
|
+
const CDS = global.cds;
|
|
501
|
+
const { DELETE } = CDS.ql;
|
|
502
|
+
const svc = await resolveServiceInstance(resAnno.serviceName);
|
|
503
|
+
if (!svc) {
|
|
504
|
+
const msg = `Service not found: ${resAnno.serviceName}. Available: ${Object.keys(CDS.services || {}).join(", ")}`;
|
|
505
|
+
logger_1.LOGGER.error(msg);
|
|
506
|
+
return (0, utils_2.toolError)("ERR_MISSING_SERVICE", msg);
|
|
507
|
+
}
|
|
508
|
+
// Extract keys - similar to get/update handlers
|
|
509
|
+
const keys = {};
|
|
510
|
+
for (const [k] of resAnno.resourceKeys.entries()) {
|
|
511
|
+
let provided = args[k];
|
|
512
|
+
if (provided === undefined) {
|
|
513
|
+
// Case-insensitive key matching (like in get handler)
|
|
514
|
+
const alt = Object.entries(args || {}).find(([kk]) => String(kk).toLowerCase() === String(k).toLowerCase());
|
|
515
|
+
if (alt)
|
|
516
|
+
provided = args[alt[0]];
|
|
517
|
+
}
|
|
518
|
+
if (provided === undefined) {
|
|
519
|
+
logger_1.LOGGER.warn(`Delete tool missing required key`, { key: k, toolName });
|
|
520
|
+
return (0, utils_2.toolError)("MISSING_KEY", `Missing key '${k}'`);
|
|
521
|
+
}
|
|
522
|
+
// Coerce numeric strings (like in get handler)
|
|
523
|
+
const raw = provided;
|
|
524
|
+
keys[k] =
|
|
525
|
+
typeof raw === "string" && /^\d+$/.test(raw) ? Number(raw) : raw;
|
|
526
|
+
}
|
|
527
|
+
logger_1.LOGGER.debug(`Executing DELETE on ${resAnno.target} with keys`, keys);
|
|
528
|
+
const tx = svc.tx({ user: (0, utils_1.getAccessRights)(authEnabled) });
|
|
529
|
+
try {
|
|
530
|
+
const response = await withTimeout(tx.run(DELETE.from(resAnno.target).where(keys)), TIMEOUT_MS, toolName, async () => {
|
|
531
|
+
try {
|
|
532
|
+
await tx.rollback();
|
|
533
|
+
}
|
|
534
|
+
catch { }
|
|
535
|
+
});
|
|
536
|
+
try {
|
|
537
|
+
await tx.commit();
|
|
538
|
+
}
|
|
539
|
+
catch { }
|
|
540
|
+
return (0, utils_2.asMcpResult)(response ?? { deleted: true });
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
try {
|
|
544
|
+
await tx.rollback();
|
|
545
|
+
}
|
|
546
|
+
catch { }
|
|
547
|
+
const isTimeout = String(error?.message || "").includes("timed out");
|
|
548
|
+
const msg = isTimeout
|
|
549
|
+
? `${toolName} timed out after ${TIMEOUT_MS}ms`
|
|
550
|
+
: `DELETE_FAILED: ${error?.message || String(error)}`;
|
|
551
|
+
logger_1.LOGGER.error(msg, error);
|
|
552
|
+
return (0, utils_2.toolError)(isTimeout ? "TIMEOUT" : "DELETE_FAILED", msg);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
server.registerTool(toolName, { title: toolName, description: desc, inputSchema }, deleteHandler);
|
|
556
|
+
}
|
|
477
557
|
// Helper: compile structured inputs into a CDS query
|
|
478
558
|
// The function translates the validated MCP input into CQN safely,
|
|
479
559
|
// including a basic escape of string literals to avoid invalid syntax.
|
package/lib/mcp/factory.js
CHANGED
|
@@ -24,7 +24,7 @@ function createMcpServer(config, annotations) {
|
|
|
24
24
|
name: config.name,
|
|
25
25
|
version: config.version,
|
|
26
26
|
capabilities: config.capabilities,
|
|
27
|
-
});
|
|
27
|
+
}, { instructions: config.instructions });
|
|
28
28
|
if (!annotations) {
|
|
29
29
|
logger_1.LOGGER.debug("No annotations provided, skipping registration...");
|
|
30
30
|
return server;
|
|
@@ -33,20 +33,26 @@ function createMcpServer(config, annotations) {
|
|
|
33
33
|
const authEnabled = (0, utils_1.isAuthEnabled)(config.auth);
|
|
34
34
|
// Always register discovery tool for better model planning
|
|
35
35
|
(0, describe_model_1.registerDescribeModelTool)(server);
|
|
36
|
+
const accessRights = (0, utils_1.getAccessRights)(authEnabled);
|
|
36
37
|
for (const entry of annotations.values()) {
|
|
37
38
|
if (entry instanceof structures_1.McpToolAnnotation) {
|
|
39
|
+
if (!(0, utils_1.hasToolOperationAccess)(accessRights, entry.restrictions))
|
|
40
|
+
continue;
|
|
38
41
|
(0, tools_1.assignToolToServer)(entry, server, authEnabled);
|
|
39
42
|
continue;
|
|
40
43
|
}
|
|
41
44
|
else if (entry instanceof structures_1.McpResourceAnnotation) {
|
|
42
|
-
(0,
|
|
45
|
+
const accesses = (0, utils_1.getWrapAccesses)(accessRights, entry.restrictions);
|
|
46
|
+
if (accesses.canRead) {
|
|
47
|
+
(0, resources_1.assignResourceToServer)(entry, server, authEnabled);
|
|
48
|
+
}
|
|
43
49
|
// Optionally expose entities as tools based on global/per-entity switches
|
|
44
50
|
const globalWrap = !!config.wrap_entities_to_actions;
|
|
45
51
|
const localWrap = entry.wrap?.tools;
|
|
46
52
|
const enabled = localWrap === true || (localWrap === undefined && globalWrap);
|
|
47
53
|
if (enabled) {
|
|
48
54
|
const modes = config.wrap_entity_modes ?? ["query", "get"];
|
|
49
|
-
(0, entity_tools_1.registerEntityWrappers)(entry, server, authEnabled, modes);
|
|
55
|
+
(0, entity_tools_1.registerEntityWrappers)(entry, server, authEnabled, modes, accesses);
|
|
50
56
|
}
|
|
51
57
|
continue;
|
|
52
58
|
}
|
package/lib/mcp.js
CHANGED
|
@@ -39,7 +39,7 @@ class McpPlugin {
|
|
|
39
39
|
async onBootstrap(app) {
|
|
40
40
|
logger_1.LOGGER.debug("Event received for 'bootstrap'");
|
|
41
41
|
this.expressApp = app;
|
|
42
|
-
this.expressApp.use(express_1.default.json());
|
|
42
|
+
this.expressApp.use("/mcp", express_1.default.json());
|
|
43
43
|
if (this.config.auth === "inherit") {
|
|
44
44
|
(0, utils_2.registerAuthMiddleware)(this.expressApp);
|
|
45
45
|
}
|