@gavdi/cap-mcp 1.1.1 → 1.1.3

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
@@ -1,5 +1,5 @@
1
1
  # CAP MCP Plugin - AI With Ease
2
- ![NPM Version](https://img.shields.io/npm/v/%40gavdi%2Fcap-mcp) ![NPM License](https://img.shields.io/npm/l/%40gavdi%2Fcap-mcp) ![GitHub commits since latest release](https://img.shields.io/github/commits-since/gavdilabs/cap-mcp-plugin/latest)
2
+ ![NPM Version](https://img.shields.io/npm/v/%40gavdi%2Fcap-mcp) ![NPM License](https://img.shields.io/npm/l/%40gavdi%2Fcap-mcp) ![NPM Downloads](https://img.shields.io/npm/dm/%40gavdi%2Fcap-mcp) ![GitHub commits since latest release](https://img.shields.io/github/commits-since/gavdilabs/cap-mcp-plugin/latest)
3
3
 
4
4
 
5
5
 
package/cds-plugin.js CHANGED
@@ -12,8 +12,8 @@ cds.on("bootstrap", async (app) => {
12
12
  await plugin?.onBootstrap(app);
13
13
  });
14
14
 
15
- cds.on("loaded", async (model) => {
16
- await plugin?.onLoaded(model);
15
+ cds.on("serving", async () => {
16
+ await plugin?.onLoaded(cds.model);
17
17
  });
18
18
 
19
19
  cds.on("shutdown", async () => {
@@ -126,9 +126,12 @@ function constructResourceAnnotation(serviceName, target, annotations, definitio
126
126
  if (!(0, utils_1.isValidResourceAnnotation)(annotations))
127
127
  return undefined;
128
128
  const functionalities = (0, utils_1.determineResourceOptions)(annotations);
129
+ const foreignKeys = new Map(Object.entries(model.definitions?.[`${serviceName}.${target}`].elements ?? {})
130
+ .filter(([_, v]) => v["@odata.foreignKey4"] !== undefined)
131
+ .map(([k, v]) => [k, v["@odata.foreignKey4"]]));
129
132
  const { properties, resourceKeys } = (0, utils_1.parseResourceElements)(definition, model);
130
133
  const restrictions = (0, utils_1.parseCdsRestrictions)(annotations.restrict, annotations.requires);
131
- return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys, annotations.wrap, restrictions);
134
+ return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys, foreignKeys, annotations.wrap, restrictions);
132
135
  }
133
136
  /**
134
137
  * Constructs a tool annotation from parsed annotation data
@@ -82,6 +82,8 @@ class McpResourceAnnotation extends McpAnnotation {
82
82
  _resourceKeys;
83
83
  /** Optional wrapper configuration to expose this resource as tools */
84
84
  _wrap;
85
+ /** Map of foreign keys property -> associated entity */
86
+ _foreignKeys;
85
87
  /**
86
88
  * Creates a new MCP resource annotation
87
89
  * @param name - Unique identifier for this resource
@@ -91,14 +93,17 @@ class McpResourceAnnotation extends McpAnnotation {
91
93
  * @param functionalities - Set of enabled OData query options (filter, top, skip, etc.)
92
94
  * @param properties - Map of entity properties to their CDS types
93
95
  * @param resourceKeys - Map of key fields to their types
96
+ * @param foreignKeys - Map of foreign keys used by entity
97
+ * @param wrap - Wrap usage
94
98
  * @param restrictions - Optional restrictions based on CDS roles
95
99
  */
96
- constructor(name, description, target, serviceName, functionalities, properties, resourceKeys, wrap, restrictions) {
100
+ constructor(name, description, target, serviceName, functionalities, properties, resourceKeys, foreignKeys, wrap, restrictions) {
97
101
  super(name, description, target, serviceName, restrictions ?? []);
98
102
  this._functionalities = functionalities;
99
103
  this._properties = properties;
100
104
  this._resourceKeys = resourceKeys;
101
105
  this._wrap = wrap;
106
+ this._foreignKeys = foreignKeys;
102
107
  }
103
108
  /**
104
109
  * Gets the set of enabled OData query functionalities
@@ -107,6 +112,13 @@ class McpResourceAnnotation extends McpAnnotation {
107
112
  get functionalities() {
108
113
  return this._functionalities;
109
114
  }
115
+ /**
116
+ * Gets the map of foreign keys used withing the resource
117
+ * @returns Map of foreign keys - property name -> associated entity
118
+ */
119
+ get foreignKeys() {
120
+ return this._foreignKeys;
121
+ }
110
122
  /**
111
123
  * Gets the map of entity properties to their CDS types
112
124
  * @returns Map of property names to type strings
@@ -177,18 +177,10 @@ function parseResourceElements(definition, model) {
177
177
  continue;
178
178
  }
179
179
  const result = parseParam(k, v);
180
- if (!v.key)
180
+ if (!v.key || v.type === "cds.Association")
181
181
  continue;
182
182
  resourceKeys.set(k, result);
183
183
  }
184
- // for (const [key, value] of Object.entries(definition.elements || {})) {
185
- // if (!value.type) continue;
186
- // const parsedType = value.type.replace("cds.", "");
187
- // properties.set(key, parsedType);
188
- //
189
- // if (!value.key) continue;
190
- // resourceKeys.set(key, parsedType);
191
- // }
192
184
  return {
193
185
  properties,
194
186
  resourceKeys,
@@ -83,23 +83,6 @@ function buildEnhancedQueryDescription(resAnno) {
83
83
  : "";
84
84
  return baseDesc + assocHint;
85
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
- }
103
86
  /**
104
87
  * Registers CRUD-like MCP tools for an annotated entity (resource).
105
88
  * Modes can be controlled globally via configuration and per-entity via @mcp.wrap.
@@ -158,13 +141,6 @@ function registerQueryTool(resAnno, server, authEnabled) {
158
141
  const scalarKeys = Array.from(resAnno.properties.entries())
159
142
  .filter(([, cdsType]) => !String(cdsType).toLowerCase().includes("association"))
160
143
  .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
144
  // Build where field enum: use same fields as select (scalar + foreign keys)
169
145
  // This ensures consistency - what you can select, you can filter by
170
146
  const whereKeys = [...scalarKeys];
@@ -369,16 +345,14 @@ function registerCreateTool(resAnno, server, authEnabled) {
369
345
  for (const [propName, cdsType] of resAnno.properties.entries()) {
370
346
  const isAssociation = String(cdsType).toLowerCase().includes("association");
371
347
  if (isAssociation) {
372
- // Prefer foreign key input for associations: <assoc>_ID
373
- inputSchema[`${propName}_ID`] = zod_1.z
374
- .number()
375
- .describe(`Foreign key for association ${propName}`)
376
- .optional();
348
+ // Association keys are supplied directly from model loading as of v1.1.2
377
349
  continue;
378
350
  }
379
351
  inputSchema[propName] = (0, utils_2.determineMcpParameterType)(cdsType, propName, `${resAnno.serviceName}.${resAnno.target}`)
380
352
  .optional()
381
- .describe(`Field ${propName}`);
353
+ .describe(resAnno.foreignKeys.has(propName)
354
+ ? `Foreign key to ${resAnno.foreignKeys.get(propName)} on ${propName}`
355
+ : `Field ${propName}`);
382
356
  }
383
357
  const hint = constructHintMessage(resAnno, "create");
384
358
  const desc = `Resource description: ${resAnno.description}. Create a new ${resAnno.target}. Provide fields; service applies defaults.${hint}`;
@@ -459,15 +433,14 @@ function registerUpdateTool(resAnno, server, authEnabled) {
459
433
  continue;
460
434
  const isAssociation = String(cdsType).toLowerCase().includes("association");
461
435
  if (isAssociation) {
462
- inputSchema[`${propName}_ID`] = zod_1.z
463
- .number()
464
- .describe(`Foreign key for association ${propName}`)
465
- .optional();
436
+ // Association keys are supplied directly from model loading as of v1.1.2
466
437
  continue;
467
438
  }
468
439
  inputSchema[propName] = (0, utils_2.determineMcpParameterType)(cdsType, propName, `${resAnno.serviceName}.${resAnno.target}`)
469
440
  .optional()
470
- .describe(`Field ${propName}`);
441
+ .describe(resAnno.foreignKeys.has(propName)
442
+ ? `Foreign key to ${resAnno.foreignKeys.get(propName)} on ${propName}`
443
+ : `Field ${propName}`);
471
444
  }
472
445
  const keyList = Array.from(resAnno.resourceKeys.keys()).join(", ");
473
446
  const hint = constructHintMessage(resAnno, "update");
@@ -64,7 +64,7 @@ exports.ODataQueryValidationSchemas = {
64
64
  .string()
65
65
  .min(1)
66
66
  .max(200)
67
- .regex(/^[a-zA-Z_][a-zA-Z0-9_\s]+(asc|desc)?(,\s*[a-zA-Z_][a-zA-Z0-9_\s]+(asc|desc)?)*$/i),
67
+ .regex(/^[a-zA-Z_][a-zA-Z0-9_]*(?:\s+(asc|desc))?(?:\s*,\s*[a-zA-Z_][a-zA-Z0-9_]*(?:\s+(asc|desc))?)*$/i),
68
68
  filter: zod_1.z.string().min(1).max(1000),
69
69
  };
70
70
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gavdi/cap-mcp",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "MCP Pluging for CAP",
5
5
  "keywords": [
6
6
  "MCP",
@@ -26,6 +26,10 @@
26
26
  "test": "NODE_ENV=test jest --silent --testTimeout=30000",
27
27
  "test:unit": "NODE_ENV=test jest --silent test/unit --testTimeout=30000",
28
28
  "test:integration": "NODE_ENV=test jest --silent test/integration --testTimeout=30000",
29
+ "test:coverage": "NODE_ENV=test jest --silent --coverage --testTimeout=30000",
30
+ "test:coverage:unit": "NODE_ENV=test jest --silent --coverage test/unit --testTimeout=30000",
31
+ "test:coverage:integration": "NODE_ENV=test jest --silent --coverage test/integration --testTimeout=30000",
32
+ "coverage:report": "NODE_ENV=test jest --silent --coverage --passWithNoTests --testTimeout=30000",
29
33
  "build": "tsc",
30
34
  "inspect": "npx @modelcontextprotocol/inspector",
31
35
  "prepare": "husky",
@@ -41,7 +45,7 @@
41
45
  "express": "^4"
42
46
  },
43
47
  "dependencies": {
44
- "@modelcontextprotocol/sdk": "^1.17.3",
48
+ "@modelcontextprotocol/sdk": "^1.19.1",
45
49
  "@sap/xssec": "^4.9.1",
46
50
  "helmet": "^8.1.0",
47
51
  "zod": "^3.25.67",