@gavdi/cap-mcp 0.9.4 โ 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 +38 -1
- package/cds-plugin.js +1 -1
- package/lib/annotations/constants.js +11 -1
- package/lib/annotations/parser.js +25 -7
- package/lib/annotations/structures.js +29 -6
- package/lib/annotations/utils.js +62 -0
- package/lib/auth/utils.js +60 -0
- package/lib/config/loader.js +8 -0
- package/lib/logger.js +48 -2
- package/lib/mcp/describe-model.js +103 -0
- package/lib/mcp/entity-tools.js +628 -0
- package/lib/mcp/factory.js +22 -2
- package/lib/mcp/resources.js +18 -3
- package/lib/mcp/session-manager.js +13 -2
- package/lib/mcp/tools.js +10 -18
- package/lib/mcp/utils.js +56 -0
- package/lib/mcp.js +24 -6
- package/package.json +11 -6
- 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/README.md
CHANGED
|
@@ -60,7 +60,10 @@ Add MCP configuration to your `package.json`:
|
|
|
60
60
|
"cds": {
|
|
61
61
|
"mcp": {
|
|
62
62
|
"name": "my-bookshop-mcp",
|
|
63
|
-
"auth": "inherit"
|
|
63
|
+
"auth": "inherit",
|
|
64
|
+
"wrap_entities_to_actions": false,
|
|
65
|
+
"wrap_entity_modes": ["query", "get"],
|
|
66
|
+
"instructions": "MCP server instructions for agents"
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
}
|
|
@@ -80,6 +83,13 @@ service CatalogService {
|
|
|
80
83
|
resource: ['filter', 'orderby', 'select', 'top', 'skip']
|
|
81
84
|
}
|
|
82
85
|
entity Books as projection on my.Books;
|
|
86
|
+
|
|
87
|
+
// Optionally expose Books as tools for LLMs (query/get enabled by default config)
|
|
88
|
+
annotate CatalogService.Books with @mcp.wrap: {
|
|
89
|
+
tools: true,
|
|
90
|
+
modes: ['query','get'],
|
|
91
|
+
hint: 'Use for read-only lookups of books'
|
|
92
|
+
};
|
|
83
93
|
|
|
84
94
|
@mcp: {
|
|
85
95
|
name: 'get-book-recommendations',
|
|
@@ -114,10 +124,32 @@ This plugin transforms your annotated CAP services into a fully functional MCP s
|
|
|
114
124
|
|
|
115
125
|
- **๐ Resources**: Expose CAP entities as MCP resources with OData v4 query capabilities
|
|
116
126
|
- **๐ง Tools**: Convert CAP functions and actions into executable MCP tools
|
|
127
|
+
- **๐งฉ Entity Wrappers (optional)**: Expose CAP entities as tools (`query`, `get`, and optionally `create`, `update`) for LLM tool use while keeping resources intact
|
|
117
128
|
- **๐ก Prompts**: Define reusable prompt templates for AI interactions
|
|
118
129
|
- **๐ Auto-generation**: Automatically creates MCP server endpoints based on annotations
|
|
119
130
|
- **โ๏ธ Flexible Configuration**: Support for custom parameter sets and descriptions
|
|
120
131
|
|
|
132
|
+
## ๐งช Testing & Inspector
|
|
133
|
+
|
|
134
|
+
- Run tests: `npm test`
|
|
135
|
+
- Start demo app: `npm run mock`
|
|
136
|
+
- Inspector: `npx @modelcontextprotocol/inspector`
|
|
137
|
+
|
|
138
|
+
### New wrapper tools
|
|
139
|
+
|
|
140
|
+
When `wrap_entities_to_actions` is enabled (globally or via `@mcp.wrap.tools: true`), you will see tools named like:
|
|
141
|
+
|
|
142
|
+
- `CatalogService_Books_query`
|
|
143
|
+
- `CatalogService_Books_get`
|
|
144
|
+
- `CatalogService_Books_create` (if enabled)
|
|
145
|
+
- `CatalogService_Books_update` (if enabled)
|
|
146
|
+
|
|
147
|
+
Each tool includes a description with fields and OData notes to guide the model. You can add `@mcp.wrap.hint` per entity to enrich descriptions for LLMs.
|
|
148
|
+
|
|
149
|
+
### Bruno collection
|
|
150
|
+
|
|
151
|
+
The `bruno/` folder contains HTTP requests for the MCP endpoint (handy for local manual testing using Bruno or any HTTP client). You may add calls for `tools/list` and `tools/call` to exercise the new wrapper tools.
|
|
152
|
+
|
|
121
153
|
## ๐ Usage
|
|
122
154
|
|
|
123
155
|
### Resource Annotations
|
|
@@ -220,6 +252,7 @@ Configure the MCP plugin through your CAP application's `package.json` or `.cdsr
|
|
|
220
252
|
"name": "my-mcp-server",
|
|
221
253
|
"version": "1.0.0",
|
|
222
254
|
"auth": "inherit",
|
|
255
|
+
"instructions": "mcp server instructions for agents",
|
|
223
256
|
"capabilities": {
|
|
224
257
|
"resources": {
|
|
225
258
|
"listChanged": true,
|
|
@@ -425,6 +458,10 @@ npm test -- --verbose
|
|
|
425
458
|
npm test -- --watch
|
|
426
459
|
```
|
|
427
460
|
|
|
461
|
+
### Further reading
|
|
462
|
+
|
|
463
|
+
- Short guide on entity tools and configuration: `docs/entity-tools.md`
|
|
464
|
+
|
|
428
465
|
## ๐ค Contributing
|
|
429
466
|
|
|
430
467
|
Contributions are welcome! This is an open-source project aimed at bridging CAP applications with the AI ecosystem.
|
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.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
|
|
@@ -25,6 +25,16 @@ exports.MCP_ANNOTATION_PROPS = {
|
|
|
25
25
|
MCP_TOOL: "@mcp.tool",
|
|
26
26
|
/** Prompt templates annotation for CAP services */
|
|
27
27
|
MCP_PROMPT: "@mcp.prompts",
|
|
28
|
+
/** Wrapper configuration for exposing entities as tools */
|
|
29
|
+
MCP_WRAP: "@mcp.wrap",
|
|
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",
|
|
28
38
|
};
|
|
29
39
|
/**
|
|
30
40
|
* Default set of all available OData query options for MCP resources
|
|
@@ -18,16 +18,18 @@ function parseDefinitions(model) {
|
|
|
18
18
|
}
|
|
19
19
|
const result = new Map();
|
|
20
20
|
for (const [key, value] of Object.entries(model.definitions)) {
|
|
21
|
-
|
|
21
|
+
// Narrow unknown to csn.Definition with a runtime check
|
|
22
|
+
const def = value;
|
|
23
|
+
const parsedAnnotations = parseAnnotations(def);
|
|
22
24
|
const { serviceName, target } = (0, utils_1.splitDefinitionName)(key);
|
|
23
|
-
parseBoundOperations(serviceName, target,
|
|
25
|
+
parseBoundOperations(serviceName, target, def, result); // Mutates result map with bound operations
|
|
24
26
|
if (!parsedAnnotations || !(0, utils_1.containsRequiredAnnotations)(parsedAnnotations)) {
|
|
25
27
|
continue; // This check must occur here, since we do want the bound operations even if the parent is not annotated
|
|
26
28
|
}
|
|
27
29
|
const verifiedAnnotations = parsedAnnotations;
|
|
28
|
-
switch (
|
|
30
|
+
switch (def.kind) {
|
|
29
31
|
case "entity":
|
|
30
|
-
const resourceAnnotation = constructResourceAnnotation(serviceName, target, verifiedAnnotations,
|
|
32
|
+
const resourceAnnotation = constructResourceAnnotation(serviceName, target, verifiedAnnotations, def);
|
|
31
33
|
if (!resourceAnnotation)
|
|
32
34
|
continue;
|
|
33
35
|
result.set(resourceAnnotation.target, resourceAnnotation);
|
|
@@ -68,8 +70,12 @@ function parseAnnotations(definition) {
|
|
|
68
70
|
definition: definition,
|
|
69
71
|
};
|
|
70
72
|
for (const [k, v] of Object.entries(definition)) {
|
|
71
|
-
|
|
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")) {
|
|
72
77
|
continue;
|
|
78
|
+
}
|
|
73
79
|
logger_1.LOGGER.debug("Parsing: ", k, v);
|
|
74
80
|
switch (k) {
|
|
75
81
|
case constants_1.MCP_ANNOTATION_PROPS.MCP_NAME:
|
|
@@ -87,6 +93,16 @@ function parseAnnotations(definition) {
|
|
|
87
93
|
case constants_1.MCP_ANNOTATION_PROPS.MCP_PROMPT:
|
|
88
94
|
annotations.prompts = v;
|
|
89
95
|
continue;
|
|
96
|
+
case constants_1.MCP_ANNOTATION_PROPS.MCP_WRAP:
|
|
97
|
+
// Wrapper container to expose resources as tools
|
|
98
|
+
annotations.wrap = v;
|
|
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;
|
|
90
106
|
default:
|
|
91
107
|
continue;
|
|
92
108
|
}
|
|
@@ -106,7 +122,8 @@ function constructResourceAnnotation(serviceName, target, annotations, definitio
|
|
|
106
122
|
return undefined;
|
|
107
123
|
const functionalities = (0, utils_1.determineResourceOptions)(annotations);
|
|
108
124
|
const { properties, resourceKeys } = (0, utils_1.parseResourceElements)(definition);
|
|
109
|
-
|
|
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);
|
|
110
127
|
}
|
|
111
128
|
/**
|
|
112
129
|
* Constructs a tool annotation from parsed annotation data
|
|
@@ -121,7 +138,8 @@ function constructToolAnnotation(serviceName, target, annotations, entityKey, ke
|
|
|
121
138
|
if (!(0, utils_1.isValidToolAnnotation)(annotations))
|
|
122
139
|
return undefined;
|
|
123
140
|
const { parameters, operationKind } = (0, utils_1.parseOperationElements)(annotations);
|
|
124
|
-
|
|
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);
|
|
125
143
|
}
|
|
126
144
|
/**
|
|
127
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
|
/**
|
|
@@ -68,6 +80,8 @@ class McpResourceAnnotation extends McpAnnotation {
|
|
|
68
80
|
_properties;
|
|
69
81
|
/** Map of resource key fields to their types */
|
|
70
82
|
_resourceKeys;
|
|
83
|
+
/** Optional wrapper configuration to expose this resource as tools */
|
|
84
|
+
_wrap;
|
|
71
85
|
/**
|
|
72
86
|
* Creates a new MCP resource annotation
|
|
73
87
|
* @param name - Unique identifier for this resource
|
|
@@ -77,12 +91,14 @@ class McpResourceAnnotation extends McpAnnotation {
|
|
|
77
91
|
* @param functionalities - Set of enabled OData query options (filter, top, skip, etc.)
|
|
78
92
|
* @param properties - Map of entity properties to their CDS types
|
|
79
93
|
* @param resourceKeys - Map of key fields to their types
|
|
94
|
+
* @param restrictions - Optional restrictions based on CDS roles
|
|
80
95
|
*/
|
|
81
|
-
constructor(name, description, target, serviceName, functionalities, properties, resourceKeys) {
|
|
82
|
-
super(name, description, target, serviceName);
|
|
96
|
+
constructor(name, description, target, serviceName, functionalities, properties, resourceKeys, wrap, restrictions) {
|
|
97
|
+
super(name, description, target, serviceName, restrictions ?? []);
|
|
83
98
|
this._functionalities = functionalities;
|
|
84
99
|
this._properties = properties;
|
|
85
100
|
this._resourceKeys = resourceKeys;
|
|
101
|
+
this._wrap = wrap;
|
|
86
102
|
}
|
|
87
103
|
/**
|
|
88
104
|
* Gets the set of enabled OData query functionalities
|
|
@@ -105,6 +121,12 @@ class McpResourceAnnotation extends McpAnnotation {
|
|
|
105
121
|
get resourceKeys() {
|
|
106
122
|
return this._resourceKeys;
|
|
107
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Gets the wrapper configuration for exposing this resource as tools
|
|
126
|
+
*/
|
|
127
|
+
get wrap() {
|
|
128
|
+
return this._wrap;
|
|
129
|
+
}
|
|
108
130
|
}
|
|
109
131
|
exports.McpResourceAnnotation = McpResourceAnnotation;
|
|
110
132
|
/**
|
|
@@ -130,9 +152,10 @@ class McpToolAnnotation extends McpAnnotation {
|
|
|
130
152
|
* @param entityKey - Optional entity key field for bound operations
|
|
131
153
|
* @param operationKind - Optional operation type ('function' or 'action')
|
|
132
154
|
* @param keyTypeMap - Optional map of key fields to types for bound operations
|
|
155
|
+
* @param restrictions - Optional restrictions based on CDS roles
|
|
133
156
|
*/
|
|
134
|
-
constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap) {
|
|
135
|
-
super(name, description, operation, serviceName);
|
|
157
|
+
constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap, restrictions) {
|
|
158
|
+
super(name, description, operation, serviceName, restrictions ?? []);
|
|
136
159
|
this._parameters = parameters;
|
|
137
160
|
this._entityKey = entityKey;
|
|
138
161
|
this._operationKind = operationKind;
|
|
@@ -183,7 +206,7 @@ class McpPromptAnnotation extends McpAnnotation {
|
|
|
183
206
|
* @param prompts - Array of prompt template definitions
|
|
184
207
|
*/
|
|
185
208
|
constructor(name, description, serviceName, prompts) {
|
|
186
|
-
super(name, description, serviceName, serviceName);
|
|
209
|
+
super(name, description, serviceName, serviceName, []);
|
|
187
210
|
this._prompts = prompts;
|
|
188
211
|
}
|
|
189
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
|
@@ -28,6 +28,14 @@ function loadConfiguration() {
|
|
|
28
28
|
resources: cdsEnv?.capabilities?.resources ?? { listChanged: true },
|
|
29
29
|
prompts: cdsEnv?.capabilities?.prompts ?? { listChanged: true },
|
|
30
30
|
},
|
|
31
|
+
wrap_entities_to_actions: cdsEnv?.wrap_entities_to_actions ?? false,
|
|
32
|
+
wrap_entity_modes: cdsEnv?.wrap_entity_modes ?? [
|
|
33
|
+
"query",
|
|
34
|
+
"get",
|
|
35
|
+
"create",
|
|
36
|
+
"update",
|
|
37
|
+
],
|
|
38
|
+
instructions: cdsEnv?.instructions,
|
|
31
39
|
};
|
|
32
40
|
}
|
|
33
41
|
/**
|
package/lib/logger.js
CHANGED
|
@@ -7,8 +7,54 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.LOGGER = void 0;
|
|
8
8
|
/* @ts-ignore */
|
|
9
9
|
const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
|
|
10
|
+
// In some test/mocked environments cds.log may not exist. Provide a no-op fallback.
|
|
11
|
+
const safeLog = (ns) => {
|
|
12
|
+
try {
|
|
13
|
+
if (typeof cds?.log === "function")
|
|
14
|
+
return cds.log(ns);
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
return {
|
|
18
|
+
debug: () => { },
|
|
19
|
+
info: () => { },
|
|
20
|
+
warn: () => { },
|
|
21
|
+
error: () => { },
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
// Create both channels so logs show up even if the app configured "mcp" instead of "cds-mcp"
|
|
25
|
+
const loggerPrimary = safeLog("cds-mcp");
|
|
26
|
+
const loggerCompat = safeLog("mcp");
|
|
10
27
|
/**
|
|
11
28
|
* Shared logger instance for all MCP plugin components
|
|
12
|
-
*
|
|
29
|
+
* Multiplexes logs to both "cds-mcp" and legacy "mcp" channels for visibility
|
|
13
30
|
*/
|
|
14
|
-
exports.LOGGER =
|
|
31
|
+
exports.LOGGER = {
|
|
32
|
+
debug: (...args) => {
|
|
33
|
+
try {
|
|
34
|
+
loggerPrimary?.debug?.(...args);
|
|
35
|
+
loggerCompat?.debug?.(...args);
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
},
|
|
39
|
+
info: (...args) => {
|
|
40
|
+
try {
|
|
41
|
+
loggerPrimary?.info?.(...args);
|
|
42
|
+
loggerCompat?.info?.(...args);
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
},
|
|
46
|
+
warn: (...args) => {
|
|
47
|
+
try {
|
|
48
|
+
loggerPrimary?.warn?.(...args);
|
|
49
|
+
loggerCompat?.warn?.(...args);
|
|
50
|
+
}
|
|
51
|
+
catch { }
|
|
52
|
+
},
|
|
53
|
+
error: (...args) => {
|
|
54
|
+
try {
|
|
55
|
+
loggerPrimary?.error?.(...args);
|
|
56
|
+
loggerCompat?.error?.(...args);
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerDescribeModelTool = registerDescribeModelTool;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
/**
|
|
7
|
+
* Registers a discovery tool that describes CAP services/entities and fields.
|
|
8
|
+
* Helpful for models to plan correct tool calls without trial-and-error.
|
|
9
|
+
*/
|
|
10
|
+
function registerDescribeModelTool(server) {
|
|
11
|
+
const inputZod = zod_1.z
|
|
12
|
+
.object({
|
|
13
|
+
service: zod_1.z.string().optional(),
|
|
14
|
+
entity: zod_1.z.string().optional(),
|
|
15
|
+
format: zod_1.z.enum(["concise", "detailed"]).default("concise").optional(),
|
|
16
|
+
})
|
|
17
|
+
.strict();
|
|
18
|
+
const inputSchema = {
|
|
19
|
+
service: inputZod.shape.service,
|
|
20
|
+
entity: inputZod.shape.entity,
|
|
21
|
+
format: inputZod.shape.format,
|
|
22
|
+
};
|
|
23
|
+
server.registerTool("cap_describe_model", {
|
|
24
|
+
title: "cap_describe_model",
|
|
25
|
+
description: "Describe CAP services/entities and their fields, keys, and example tool calls. Use this to guide LLMs how to call entity wrapper tools.",
|
|
26
|
+
inputSchema,
|
|
27
|
+
}, async (rawArgs) => {
|
|
28
|
+
const args = inputZod.parse(rawArgs);
|
|
29
|
+
const CDS = global.cds;
|
|
30
|
+
const refl = CDS.reflect(CDS.model);
|
|
31
|
+
const listServices = () => {
|
|
32
|
+
const names = Object.values(CDS.services || {})
|
|
33
|
+
.map((s) => s?.definition?.name || s?.name)
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
return { services: [...new Set(names)].sort() };
|
|
36
|
+
};
|
|
37
|
+
const listEntities = (service) => {
|
|
38
|
+
const all = Object.values(refl.entities || {}) || [];
|
|
39
|
+
const filtered = service
|
|
40
|
+
? all.filter((e) => String(e.name).startsWith(service + "."))
|
|
41
|
+
: all;
|
|
42
|
+
return {
|
|
43
|
+
entities: filtered.map((e) => e.name).sort(),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
const describeEntity = (service, entity) => {
|
|
47
|
+
if (!entity)
|
|
48
|
+
return { error: "Please provide 'entity'." };
|
|
49
|
+
const fqn = service && !entity.includes(".") ? `${service}.${entity}` : entity;
|
|
50
|
+
const ent = (refl.entities || {})[fqn] || (refl.entities || {})[entity];
|
|
51
|
+
if (!ent)
|
|
52
|
+
return {
|
|
53
|
+
error: `Entity not found: ${entity}${service ? ` (service ${service})` : ""}`,
|
|
54
|
+
};
|
|
55
|
+
const elements = Object.entries(ent.elements || {}).map(([name, el]) => ({
|
|
56
|
+
name,
|
|
57
|
+
type: el.type,
|
|
58
|
+
key: !!el.key,
|
|
59
|
+
target: el.target || undefined,
|
|
60
|
+
isArray: !!el.items,
|
|
61
|
+
}));
|
|
62
|
+
const keys = elements.filter((e) => e.key).map((e) => e.name);
|
|
63
|
+
const sampleTop = 5;
|
|
64
|
+
const shortFields = elements.slice(0, 5).map((e) => e.name);
|
|
65
|
+
// Match wrapper tool naming: Service_Entity_mode
|
|
66
|
+
const entName = String(ent?.name || "entity");
|
|
67
|
+
const svcPart = service || entName.split(".")[0] || "Service";
|
|
68
|
+
const entityBase = entName.split(".").pop() || "Entity";
|
|
69
|
+
const listName = `${svcPart}_${entityBase}_query`;
|
|
70
|
+
const getName = `${svcPart}_${entityBase}_get`;
|
|
71
|
+
return {
|
|
72
|
+
service,
|
|
73
|
+
entity: ent.name,
|
|
74
|
+
keys,
|
|
75
|
+
fields: elements,
|
|
76
|
+
usage: {
|
|
77
|
+
rationale: "Entity wrapper tools expose CRUD-like operations for LLMs. Prefer query/get globally; create/update must be explicitly enabled by the developer.",
|
|
78
|
+
guidance: "Use the *_query tool for retrieval with filters and projections; use *_get with keys for a single record; use *_create/*_update only if enabled and necessary.",
|
|
79
|
+
},
|
|
80
|
+
examples: {
|
|
81
|
+
list_tool: listName,
|
|
82
|
+
list_tool_payload: {
|
|
83
|
+
top: sampleTop,
|
|
84
|
+
select: shortFields,
|
|
85
|
+
},
|
|
86
|
+
get_tool: getName,
|
|
87
|
+
get_tool_payload: keys.length ? { [keys[0]]: "<value>" } : {},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
let json;
|
|
92
|
+
if (!args.service && !args.entity) {
|
|
93
|
+
json = { ...listServices(), ...listEntities(undefined) };
|
|
94
|
+
}
|
|
95
|
+
else if (args.service && !args.entity) {
|
|
96
|
+
json = { service: args.service, ...listEntities(args.service) };
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
json = describeEntity(args.service, args.entity);
|
|
100
|
+
}
|
|
101
|
+
return (0, utils_1.asMcpResult)(json);
|
|
102
|
+
});
|
|
103
|
+
}
|