@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 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,4 +1,4 @@
1
- const cds = global.cds || require("@sap/cds");
1
+ const cds = global.cds; // enforce host app cds instance
2
2
  const McpPlugin = require("./lib/mcp").default;
3
3
 
4
4
  const plugin = new McpPlugin();
@@ -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
- const parsedAnnotations = parseAnnotations(value);
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, value, result); // Mutates result map with bound operations
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 (value.kind) {
30
+ switch (def.kind) {
29
31
  case "entity":
30
- const resourceAnnotation = constructResourceAnnotation(serviceName, target, verifiedAnnotations, value);
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
- if (!k.includes(constants_1.MCP_ANNOTATION_KEY))
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
- return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys);
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
- return new structures_1.McpToolAnnotation(annotations.name, annotations.description, target, serviceName, parameters, entityKey, operationKind, keyParams);
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
  /**
@@ -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
+ }
@@ -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
- * Provides debug, info, warn, and error logging methods
29
+ * Multiplexes logs to both "cds-mcp" and legacy "mcp" channels for visibility
13
30
  */
14
- exports.LOGGER = cds.log("cds-mcp");
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
+ }