@gavdi/cap-mcp 1.0.2 → 1.1.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/cds-plugin.js CHANGED
@@ -1,5 +1,9 @@
1
1
  const cds = global.cds; // enforce host app cds instance
2
2
  const McpPlugin = require("./lib/mcp").default;
3
+ const McpBuild = require("./lib/config/build");
4
+
5
+ // Build tasks
6
+ McpBuild.registerBuildTask();
3
7
 
4
8
  const plugin = new McpPlugin();
5
9
 
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_ALL_RESOURCE_OPTIONS = exports.CDS_AUTH_ANNOTATIONS = exports.MCP_ANNOTATION_PROPS = exports.MCP_ANNOTATION_KEY = void 0;
3
+ exports.DEFAULT_ALL_RESOURCE_OPTIONS = exports.MCP_ANNOTATION_MAPPING = 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
@@ -11,37 +11,27 @@ exports.DEFAULT_ALL_RESOURCE_OPTIONS = exports.CDS_AUTH_ANNOTATIONS = exports.MC
11
11
  */
12
12
  exports.MCP_ANNOTATION_KEY = "@mcp";
13
13
  /**
14
- * Complete set of supported MCP annotation property names
15
- * Maps logical names to their actual annotation keys used in CDS files
14
+ * Mapping of the custom annotations + CDS specific annotations and their correlated mapping for MCP usage
16
15
  */
17
- exports.MCP_ANNOTATION_PROPS = {
18
- /** Name identifier annotation - required for all MCP elements */
19
- MCP_NAME: "@mcp.name",
20
- /** Description annotation - required for all MCP elements */
21
- MCP_DESCRIPTION: "@mcp.description",
22
- /** Resource configuration annotation for CAP entities */
23
- MCP_RESOURCE: "@mcp.resource",
24
- /** Tool configuration annotation for CAP functions/actions */
25
- MCP_TOOL: "@mcp.tool",
26
- /** Prompt templates annotation for CAP services */
27
- MCP_PROMPT: "@mcp.prompts",
28
- /** Wrapper configuration for exposing entities as tools - tools prop*/
29
- MCP_WRAP_TOOLS: "@mcp.wrap.tools",
30
- /** Wrapper configuration for exposing entities as tools - modes prop*/
31
- MCP_WRAP_MODES: "@mcp.wrap.modes",
32
- /** Wrapper configuration for exposing entities as tools - hint prop*/
33
- MCP_WRAP_HINT: "@mcp.wrap.hint",
34
- /** Elicited user input annotation for tools in CAP services */
35
- MCP_ELICIT: "@mcp.elicit",
36
- };
37
- /**
38
- * Set of annotations used for CDS auth annotations
39
- * Maps logical names to their actual annotation keys used in CDS files.
40
- */
41
- exports.CDS_AUTH_ANNOTATIONS = {
42
- REQUIRES: "@requires",
43
- RESTRICT: "@restrict",
44
- };
16
+ exports.MCP_ANNOTATION_MAPPING = new Map([
17
+ ["@mcp.name", "name"],
18
+ ["@mcp.description", "description"],
19
+ ["@mcp.resource", "resource"],
20
+ ["@mcp.tool", "tool"],
21
+ ["@mcp.prompts", "prompts"],
22
+ ["@mcp.wrap", "wrap"],
23
+ ["@mcp.wrap.tools", "wrap.tools"],
24
+ ["@mcp.wrap.modes", "wrap.modes"],
25
+ ["@mcp.wrap.hint", "wrap.hint"],
26
+ ["@mcp.wrap.hint.get", "wrap.hint.get"],
27
+ ["@mcp.wrap.hint.query", "wrap.hint.query"],
28
+ ["@mcp.wrap.hint.create", "wrap.hint.create"],
29
+ ["@mcp.wrap.hint.update", "wrap.hint.update"],
30
+ ["@mcp.wrap.hint.delete", "wrap.hint.delete"],
31
+ ["@mcp.elicit", "elicit"],
32
+ ["@requires", "requires"],
33
+ ["@restrict", "restrict"],
34
+ ]);
45
35
  /**
46
36
  * Default set of all available OData query options for MCP resources
47
37
  * Used when @mcp.resource is set to `true` to enable all capabilities
@@ -65,6 +65,40 @@ function parseDefinitions(model) {
65
65
  }
66
66
  return result;
67
67
  }
68
+ function mapToMcpAnnotationStructure(obj) {
69
+ const result = {};
70
+ // Helper function to set nested properties
71
+ const setNestedValue = (target, path, value) => {
72
+ const keys = path.split(".");
73
+ const lastKey = keys.pop();
74
+ const nestedTarget = keys.reduce((current, key) => {
75
+ if (!(key in current)) {
76
+ current[key] = {};
77
+ }
78
+ return current[key];
79
+ }, target);
80
+ // If the target already has a value and both are objects, merge them
81
+ if (typeof nestedTarget[lastKey] === "object" &&
82
+ typeof value === "object" &&
83
+ nestedTarget[lastKey] !== null &&
84
+ value !== null &&
85
+ !Array.isArray(nestedTarget[lastKey]) &&
86
+ !Array.isArray(value)) {
87
+ nestedTarget[lastKey] = { ...nestedTarget[lastKey], ...value };
88
+ }
89
+ else {
90
+ nestedTarget[lastKey] = value;
91
+ }
92
+ };
93
+ // Loop through object keys and map them
94
+ for (const key in obj) {
95
+ if (constants_1.MCP_ANNOTATION_MAPPING.has(key)) {
96
+ const mappedPath = constants_1.MCP_ANNOTATION_MAPPING.get(key);
97
+ setNestedValue(result, mappedPath, obj[key]);
98
+ }
99
+ }
100
+ return result;
101
+ }
68
102
  /**
69
103
  * Parses MCP annotations from a definition object
70
104
  * @param definition - The definition object to parse annotations from
@@ -73,64 +107,11 @@ function parseDefinitions(model) {
73
107
  function parseAnnotations(definition) {
74
108
  if (!(0, utils_1.containsMcpAnnotation)(definition))
75
109
  return undefined;
110
+ const parsed = mapToMcpAnnotationStructure(definition);
76
111
  const annotations = {
77
112
  definition: definition,
113
+ ...parsed,
78
114
  };
79
- for (const [k, v] of Object.entries(definition)) {
80
- // Process MCP annotations and CDS auth annotations
81
- if (!k.includes(constants_1.MCP_ANNOTATION_KEY) &&
82
- !k.startsWith("@requires") &&
83
- !k.startsWith("@restrict")) {
84
- continue;
85
- }
86
- logger_1.LOGGER.debug("Parsing: ", k, v);
87
- switch (k) {
88
- case constants_1.MCP_ANNOTATION_PROPS.MCP_NAME:
89
- annotations.name = v;
90
- continue;
91
- case constants_1.MCP_ANNOTATION_PROPS.MCP_DESCRIPTION:
92
- annotations.description = v;
93
- continue;
94
- case constants_1.MCP_ANNOTATION_PROPS.MCP_RESOURCE:
95
- annotations.resource = v;
96
- continue;
97
- case constants_1.MCP_ANNOTATION_PROPS.MCP_TOOL:
98
- annotations.tool = v;
99
- continue;
100
- case constants_1.MCP_ANNOTATION_PROPS.MCP_PROMPT:
101
- annotations.prompts = v;
102
- continue;
103
- case constants_1.MCP_ANNOTATION_PROPS.MCP_WRAP_TOOLS:
104
- if (!annotations.wrap) {
105
- annotations.wrap = {};
106
- }
107
- annotations.wrap.tools = v;
108
- continue;
109
- case constants_1.MCP_ANNOTATION_PROPS.MCP_WRAP_HINT:
110
- if (!annotations.wrap) {
111
- annotations.wrap = {};
112
- }
113
- annotations.wrap.hint = v;
114
- continue;
115
- case constants_1.MCP_ANNOTATION_PROPS.MCP_WRAP_MODES:
116
- if (!annotations.wrap) {
117
- annotations.wrap = {};
118
- }
119
- annotations.wrap.modes = v;
120
- continue;
121
- case constants_1.MCP_ANNOTATION_PROPS.MCP_ELICIT:
122
- annotations.elicit = v;
123
- continue;
124
- case constants_1.CDS_AUTH_ANNOTATIONS.REQUIRES:
125
- annotations.requires = v;
126
- continue;
127
- case constants_1.CDS_AUTH_ANNOTATIONS.RESTRICT:
128
- annotations.restrict = v;
129
- continue;
130
- default:
131
- continue;
132
- }
133
- }
134
115
  return annotations;
135
116
  }
136
117
  /**
@@ -177,21 +177,23 @@ function parseResourceElements(definition) {
177
177
  */
178
178
  function parseOperationElements(annotations, model) {
179
179
  let parameters;
180
+ const parseParam = (k, v, suffix) => {
181
+ if (typeof v.type !== "string") {
182
+ const referencedType = parseTypedReference(v.type, model);
183
+ parameters?.set(k, `${referencedType}${suffix ?? ""}`);
184
+ return;
185
+ }
186
+ parameters?.set(k, `${v.type.replace("cds.", "")}${suffix ?? ""}`);
187
+ };
180
188
  const params = annotations.definition["params"];
181
189
  if (params && Object.entries(params).length > 0) {
182
190
  parameters = new Map();
183
191
  for (const [k, v] of Object.entries(params)) {
184
- if (typeof v.type !== "string") {
185
- // const references = v.type.ref;
186
- // const typeReference =
187
- // model.definitions?.[references[0]].elements[references[1]];
188
- // parameters.set(k, typeReference?.type?.replace("cds.", "") as string);
189
- const referencedType = parseTypedReference(v.type, model);
190
- parameters.set(k, referencedType);
191
- logger_1.LOGGER.debug("Typed reference found", referencedType);
192
+ if (v.items) {
193
+ parseParam(k, v.items, "Array");
192
194
  continue;
193
195
  }
194
- parameters.set(k, v.type.replace("cds.", ""));
196
+ parseParam(k, v);
195
197
  }
196
198
  }
197
199
  return {
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerBuildTask = registerBuildTask;
4
+ const logger_1 = require("../logger");
5
+ const json_parser_1 = require("./json-parser");
6
+ /* @ts-ignore */
7
+ const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
8
+ function registerBuildTask() {
9
+ cds.build?.register("mcp", class McpBuildPlugin extends cds.build.Plugin {
10
+ static taskDefaults = {};
11
+ static instructionsPath;
12
+ static hasTask() {
13
+ const config = cds.env.mcp;
14
+ if (!config) {
15
+ return false;
16
+ }
17
+ else if (typeof config === "object") {
18
+ this.instructionsPath =
19
+ typeof config.instructions === "object"
20
+ ? config.instructions.file
21
+ : undefined;
22
+ return (this.instructionsPath !== undefined &&
23
+ this.instructionsPath.length > 0);
24
+ }
25
+ const parsed = (0, json_parser_1.parseCAPConfiguration)(config);
26
+ if (!parsed || typeof parsed.instructions !== "object") {
27
+ return false;
28
+ }
29
+ this.instructionsPath =
30
+ typeof parsed.instructions === "object"
31
+ ? parsed.instructions.file
32
+ : undefined;
33
+ return (this.instructionsPath !== undefined &&
34
+ this.instructionsPath.length > 0);
35
+ }
36
+ async build() {
37
+ logger_1.LOGGER.debug("Performing build task - copy MCP instructions");
38
+ if (!McpBuildPlugin.instructionsPath) {
39
+ return;
40
+ }
41
+ if (cds.utils.fs.existsSync(this.task.src, McpBuildPlugin.instructionsPath)) {
42
+ await this.copy(McpBuildPlugin.instructionsPath).to(cds.utils.path.join("srv", McpBuildPlugin.instructionsPath));
43
+ }
44
+ }
45
+ });
46
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMcpInstructions = getMcpInstructions;
4
+ exports.readInstructionsFile = readInstructionsFile;
5
+ const cds_1 = require("@sap/cds");
6
+ function getMcpInstructions(config) {
7
+ if (!config.instructions) {
8
+ return undefined;
9
+ }
10
+ if (typeof config.instructions === "string") {
11
+ return config.instructions;
12
+ }
13
+ return config.instructions.file
14
+ ? readInstructionsFile(config.instructions.file)
15
+ : undefined;
16
+ }
17
+ function readInstructionsFile(path) {
18
+ if (!containsMarkdownType(path)) {
19
+ throw new Error("Invalid file type provided for instructions");
20
+ }
21
+ else if (!cds_1.utils.fs.existsSync(path)) {
22
+ throw new Error("Instructions file not found");
23
+ }
24
+ const file = cds_1.utils.fs.readFileSync(path);
25
+ return file.toString("utf8");
26
+ }
27
+ function containsMarkdownType(path) {
28
+ const extension = path.substring(path.length - 3);
29
+ return extension === ".md";
30
+ }
@@ -247,8 +247,8 @@ function registerQueryTool(resAnno, server, authEnabled) {
247
247
  aggregate: inputZod.shape.aggregate,
248
248
  explain: inputZod.shape.explain,
249
249
  };
250
- const hint = resAnno.wrap?.hint ? ` Hint: ${resAnno.wrap?.hint}` : "";
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.` +
250
+ const hint = constructHintMessage(resAnno, "query");
251
+ const desc = `Resource description: ${resAnno.description}. ${buildEnhancedQueryDescription(resAnno)} CRITICAL: Use foreign key fields (e.g., author_ID) for associations - association names (e.g., author) won't work in filters.` +
252
252
  hint;
253
253
  const queryHandler = async (rawArgs) => {
254
254
  const parsed = inputZod.safeParse(rawArgs);
@@ -298,8 +298,8 @@ function registerGetTool(resAnno, server, authEnabled) {
298
298
  inputSchema[k] = (0, utils_2.determineMcpParameterType)(cdsType).describe(`Key ${k}`);
299
299
  }
300
300
  const keyList = Array.from(resAnno.resourceKeys.keys()).join(", ");
301
- const hint = resAnno.wrap?.hint ? ` Hint: ${resAnno.wrap?.hint}` : "";
302
- const desc = `Get one ${resAnno.target} by key(s): ${keyList}. For fields & examples call cap_describe_model.${hint}`;
301
+ const hint = constructHintMessage(resAnno, "get");
302
+ const desc = `Resource description: ${resAnno.description}. Get one ${resAnno.target} by key(s): ${keyList}. For fields & examples call cap_describe_model.${hint}`;
303
303
  const getHandler = async (args) => {
304
304
  const startTime = Date.now();
305
305
  const CDS = global.cds;
@@ -380,8 +380,8 @@ function registerCreateTool(resAnno, server, authEnabled) {
380
380
  .optional()
381
381
  .describe(`Field ${propName}`);
382
382
  }
383
- const hint = resAnno.wrap?.hint ? ` Hint: ${resAnno.wrap?.hint}` : "";
384
- const desc = `Create a new ${resAnno.target}. Provide fields; service applies defaults.${hint}`;
383
+ const hint = constructHintMessage(resAnno, "create");
384
+ const desc = `Resource description: ${resAnno.description}. Create a new ${resAnno.target}. Provide fields; service applies defaults.${hint}`;
385
385
  const createHandler = async (args) => {
386
386
  const CDS = global.cds;
387
387
  const { INSERT } = CDS.ql;
@@ -470,8 +470,8 @@ function registerUpdateTool(resAnno, server, authEnabled) {
470
470
  .describe(`Field ${propName}`);
471
471
  }
472
472
  const keyList = Array.from(resAnno.resourceKeys.keys()).join(", ");
473
- const hint = resAnno.wrap?.hint ? ` Hint: ${resAnno.wrap?.hint}` : "";
474
- const desc = `Update ${resAnno.target} by key(s): ${keyList}. Provide fields to update.${hint}`;
473
+ const hint = constructHintMessage(resAnno, "update");
474
+ const desc = `Resource description: ${resAnno.description}. Update ${resAnno.target} by key(s): ${keyList}. Provide fields to update.${hint}`;
475
475
  const updateHandler = async (args) => {
476
476
  const CDS = global.cds;
477
477
  const { UPDATE } = CDS.ql;
@@ -559,8 +559,8 @@ function registerDeleteTool(resAnno, server, authEnabled) {
559
559
  inputSchema[k] = (0, utils_2.determineMcpParameterType)(cdsType).describe(`Key ${k}`);
560
560
  }
561
561
  const keyList = Array.from(resAnno.resourceKeys.keys()).join(", ");
562
- const hint = resAnno.wrap?.hint ? ` Hint: ${resAnno.wrap?.hint}` : "";
563
- const desc = `Delete ${resAnno.target} by key(s): ${keyList}. This operation cannot be undone.${hint}`;
562
+ const hint = constructHintMessage(resAnno, "delete");
563
+ const desc = `Resource description: ${resAnno.description}. Delete ${resAnno.target} by key(s): ${keyList}. This operation cannot be undone.${hint}`;
564
564
  const deleteHandler = async (args) => {
565
565
  const CDS = global.cds;
566
566
  const { DELETE } = CDS.ql;
@@ -703,3 +703,15 @@ async function executeQuery(CDS, svc, args, baseQuery) {
703
703
  return svc.run(baseQuery);
704
704
  }
705
705
  }
706
+ function constructHintMessage(resAnno, wrapAction) {
707
+ if (!resAnno.wrap?.hint) {
708
+ return "";
709
+ }
710
+ else if (typeof resAnno.wrap.hint === "string") {
711
+ return ` Hint: ${resAnno.wrap?.hint}`;
712
+ }
713
+ if (typeof resAnno.wrap.hint !== "object") {
714
+ throw new Error(`Unparseable hint provided for entity: ${resAnno.name}`);
715
+ }
716
+ return ` Hint: ${resAnno.wrap.hint[wrapAction] ?? ""}`;
717
+ }
@@ -12,6 +12,7 @@ const utils_1 = require("../auth/utils");
12
12
  // Use relative import without extension for ts-jest resolver compatibility
13
13
  const entity_tools_1 = require("./entity-tools");
14
14
  const describe_model_1 = require("./describe-model");
15
+ const instructions_1 = require("../config/instructions");
15
16
  /**
16
17
  * Creates and configures an MCP server instance with the given configuration and annotations
17
18
  * @param config - CAP configuration object
@@ -24,7 +25,7 @@ function createMcpServer(config, annotations) {
24
25
  name: config.name,
25
26
  version: config.version,
26
27
  capabilities: config.capabilities,
27
- }, { instructions: config.instructions });
28
+ }, { instructions: (0, instructions_1.getMcpInstructions)(config) });
28
29
  if (!annotations) {
29
30
  logger_1.LOGGER.debug("No annotations provided, skipping registration...");
30
31
  return server;
package/lib/mcp/utils.js CHANGED
@@ -16,10 +16,76 @@ function determineMcpParameterType(cdsType) {
16
16
  switch (cdsType) {
17
17
  case "String":
18
18
  return zod_1.z.string();
19
+ case "UUID":
20
+ return zod_1.z.string();
21
+ case "Date":
22
+ return zod_1.z.date();
23
+ case "Time":
24
+ return zod_1.z.date();
25
+ case "DateTime":
26
+ return zod_1.z.date();
27
+ case "Timestamp":
28
+ return zod_1.z.number();
19
29
  case "Integer":
20
30
  return zod_1.z.number();
31
+ case "Int16":
32
+ return zod_1.z.number();
33
+ case "Int32":
34
+ return zod_1.z.number();
35
+ case "Int64":
36
+ return zod_1.z.number();
37
+ case "UInt8":
38
+ return zod_1.z.number();
39
+ case "Decimal":
40
+ return zod_1.z.number();
41
+ case "Double":
42
+ return zod_1.z.number();
21
43
  case "Boolean":
22
44
  return zod_1.z.boolean();
45
+ case "Binary":
46
+ return zod_1.z.string();
47
+ case "LargeBinary":
48
+ return zod_1.z.string();
49
+ case "LargeString":
50
+ return zod_1.z.string();
51
+ case "Map":
52
+ return zod_1.z.any();
53
+ case "StringArray":
54
+ return zod_1.z.array(zod_1.z.string());
55
+ case "DateArray":
56
+ return zod_1.z.array(zod_1.z.date());
57
+ case "TimeArray":
58
+ return zod_1.z.array(zod_1.z.date());
59
+ case "DateTimeArray":
60
+ return zod_1.z.array(zod_1.z.date());
61
+ case "TimestampArray":
62
+ return zod_1.z.array(zod_1.z.number());
63
+ case "UUIDArray":
64
+ return zod_1.z.array(zod_1.z.string());
65
+ case "IntegerArray":
66
+ return zod_1.z.array(zod_1.z.number());
67
+ case "Int16Array":
68
+ return zod_1.z.array(zod_1.z.number());
69
+ case "Int32Array":
70
+ return zod_1.z.array(zod_1.z.number());
71
+ case "Int64Array":
72
+ return zod_1.z.array(zod_1.z.number());
73
+ case "UInt8Array":
74
+ return zod_1.z.array(zod_1.z.number());
75
+ case "DecimalArray":
76
+ return zod_1.z.array(zod_1.z.number());
77
+ case "BooleanArray":
78
+ return zod_1.z.array(zod_1.z.boolean());
79
+ case "DoubleArray":
80
+ return zod_1.z.array(zod_1.z.number());
81
+ case "BinaryArray":
82
+ return zod_1.z.array(zod_1.z.string());
83
+ case "LargeBinaryArray":
84
+ return zod_1.z.array(zod_1.z.string());
85
+ case "LargeStringArray":
86
+ return zod_1.z.array(zod_1.z.string());
87
+ case "MapArray":
88
+ return zod_1.z.array(zod_1.z.any());
23
89
  default:
24
90
  return zod_1.z.string();
25
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gavdi/cap-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "MCP Pluging for CAP",
5
5
  "keywords": [
6
6
  "MCP",