@gavdi/cap-mcp 0.9.0 → 0.9.1

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.
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loadConfiguration = loadConfiguration;
4
4
  const logger_1 = require("../logger");
5
+ const env_sanitizer_1 = require("./env-sanitizer");
6
+ const json_parser_1 = require("./json-parser");
5
7
  /* @ts-ignore */
6
8
  const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
7
9
  const ENV_NPM_PACKAGE_NAME = "npm_package_name";
@@ -10,6 +12,10 @@ const DEFAULT_PROJECT_INFO = {
10
12
  name: "cap-mcp-server",
11
13
  version: "1.0.0",
12
14
  };
15
+ /**
16
+ * Loads CAP configuration from environment and CDS settings
17
+ * @returns Complete CAP configuration object with defaults applied
18
+ */
13
19
  function loadConfiguration() {
14
20
  const packageInfo = getProjectInfo();
15
21
  const cdsEnv = loadCdsEnvConfiguration();
@@ -24,17 +30,15 @@ function loadConfiguration() {
24
30
  };
25
31
  }
26
32
  /**
27
- * Retrieves the current runtime's project information.
28
- * This is used to distinguish the MCP server, by associating it with its parent application.
29
- *
30
- * In case of an error, the project info will default to plugin defaults.
31
- * See constants for reference.
33
+ * Extracts project information from environment variables with fallback to defaults
34
+ * Uses npm package environment variables to identify the hosting CAP application
35
+ * @returns Project information object with name and version
32
36
  */
33
37
  function getProjectInfo() {
34
38
  try {
35
39
  return {
36
- name: process.env[ENV_NPM_PACKAGE_NAME] ?? DEFAULT_PROJECT_INFO.name,
37
- version: process.env[ENV_NPM_PACKAGE_VERSION] ?? DEFAULT_PROJECT_INFO.version,
40
+ name: (0, env_sanitizer_1.getSafeEnvVar)(ENV_NPM_PACKAGE_NAME, DEFAULT_PROJECT_INFO.name),
41
+ version: (0, env_sanitizer_1.getSafeEnvVar)(ENV_NPM_PACKAGE_VERSION, DEFAULT_PROJECT_INFO.version),
38
42
  };
39
43
  }
40
44
  catch (e) {
@@ -42,17 +46,21 @@ function getProjectInfo() {
42
46
  return DEFAULT_PROJECT_INFO;
43
47
  }
44
48
  }
49
+ /**
50
+ * Loads CDS environment configuration from cds.env.mcp
51
+ * @returns CAP configuration object or undefined if not found/invalid
52
+ */
45
53
  function loadCdsEnvConfiguration() {
46
54
  const config = cds.env.mcp;
47
55
  if (!config)
48
56
  return undefined;
49
57
  else if (typeof config === "object")
50
58
  return config;
51
- try {
52
- return JSON.parse(config);
53
- }
54
- catch (_) {
55
- logger_1.LOGGER.warn("Could not parse the configuration object from cdsrc");
59
+ // Use secure JSON parser for string configurations
60
+ const parsed = (0, json_parser_1.parseCAPConfiguration)(config);
61
+ if (!parsed) {
62
+ logger_1.LOGGER.warn((0, json_parser_1.createSafeErrorMessage)("CDS environment configuration"));
56
63
  return undefined;
57
64
  }
65
+ return parsed;
58
66
  }
package/lib/logger.js CHANGED
@@ -1,6 +1,14 @@
1
1
  "use strict";
2
+ /**
3
+ * Logger instance for the CDS MCP plugin
4
+ * Uses CAP's built-in logging system with "cds-mcp" namespace
5
+ */
2
6
  Object.defineProperty(exports, "__esModule", { value: true });
3
7
  exports.LOGGER = void 0;
4
8
  /* @ts-ignore */
5
9
  const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
10
+ /**
11
+ * Shared logger instance for all MCP plugin components
12
+ * Provides debug, info, warn, and error logging methods
13
+ */
6
14
  exports.LOGGER = cds.log("cds-mcp");
@@ -1,6 +1,22 @@
1
1
  "use strict";
2
+ /**
3
+ * Constants used throughout the MCP (Model Context Protocol) implementation
4
+ * Defines error messages, HTTP headers, and formatting constants
5
+ */
2
6
  Object.defineProperty(exports, "__esModule", { value: true });
3
7
  exports.NEW_LINE = exports.MCP_SESSION_HEADER = exports.ERR_MISSING_SERVICE = void 0;
8
+ /**
9
+ * Standard error message returned when a CAP service cannot be found or accessed
10
+ * Used in tool execution when the target service is unavailable
11
+ */
4
12
  exports.ERR_MISSING_SERVICE = "Error: Service could not be found";
13
+ /**
14
+ * HTTP header name used to identify MCP session IDs in requests
15
+ * Client must include this header with a valid session ID for authenticated requests
16
+ */
5
17
  exports.MCP_SESSION_HEADER = "mcp-session-id";
18
+ /**
19
+ * Newline character constant used for consistent text formatting
20
+ * Used in resource descriptions and error message formatting
21
+ */
6
22
  exports.NEW_LINE = "\n";
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ /**
3
+ * Custom URI template implementation that fixes the MCP SDK's broken
4
+ * URI template matching for grouped query parameters.
5
+ *
6
+ * This is duck typing implementation of the ResourceTemplate class.
7
+ * See @modelcontextprotocol/sdk/server/mcp.js
8
+ *
9
+ * This is only a temporary solution, as we should use the official implementation from the SDK
10
+ * Upon the SDK being fixed, we should switch over to that implementation.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.CustomResourceTemplate = exports.CustomUriTemplate = void 0;
14
+ // TODO: Get rid of 'any' typing for better type safety
15
+ /**
16
+ * Custom URI template class that properly handles grouped query parameters
17
+ * in the format {?param1,param2,param3}
18
+ */
19
+ class CustomUriTemplate {
20
+ template;
21
+ baseUri = "";
22
+ queryParams = [];
23
+ constructor(template) {
24
+ this.template = template;
25
+ this.parseTemplate();
26
+ }
27
+ toString() {
28
+ return this.template;
29
+ }
30
+ parseTemplate() {
31
+ // Extract base URI and query parameters from template
32
+ // Template format: odata://CatalogService/books{?filter,orderby,select,skip,top}
33
+ const queryTemplateMatch = this.template.match(/^([^{]+)\{\?([^}]+)\}$/);
34
+ if (!queryTemplateMatch) {
35
+ // No query parameters, treat as static URI
36
+ this.baseUri = this.template;
37
+ this.queryParams = [];
38
+ return;
39
+ }
40
+ this.baseUri = queryTemplateMatch[1];
41
+ this.queryParams = queryTemplateMatch[2]
42
+ .split(",")
43
+ .map((param) => param.trim())
44
+ .filter((param) => param.length > 0);
45
+ }
46
+ /**
47
+ * Matches a URI against this template and extracts variables
48
+ * @param uri The URI to match
49
+ * @returns Object with extracted variables or null if no match
50
+ */
51
+ match(uri) {
52
+ // Check if base URI matches
53
+ if (!uri.startsWith(this.baseUri)) {
54
+ return null;
55
+ }
56
+ // Extract query string
57
+ const queryStart = uri.indexOf("?");
58
+ if (queryStart === -1) {
59
+ // No query parameters in URI
60
+ if (this.queryParams.length === 0) {
61
+ return {}; // Static URI match
62
+ }
63
+ // Template expects query params but URI has none - still valid for optional params
64
+ return {};
65
+ }
66
+ const queryString = uri.substring(queryStart + 1);
67
+ const queryPairs = queryString.split("&");
68
+ const extractedVars = {};
69
+ // Parse query parameters with strict validation
70
+ for (const pair of queryPairs) {
71
+ const equalIndex = pair.indexOf("=");
72
+ if (equalIndex > 0) {
73
+ const key = pair.substring(0, equalIndex);
74
+ const value = pair.substring(equalIndex + 1);
75
+ if (key && value !== undefined) {
76
+ const decodedKey = decodeURIComponent(key);
77
+ const decodedValue = decodeURIComponent(value);
78
+ // SECURITY: Reject entire URI if ANY unauthorized parameter is present
79
+ if (!this.queryParams.includes(decodedKey)) {
80
+ return null; // Unauthorized parameter found - reject entire URI
81
+ }
82
+ extractedVars[decodedKey] = decodedValue;
83
+ }
84
+ }
85
+ else if (pair.trim().length > 0) {
86
+ // Handle malformed parameters (missing = or empty key)
87
+ // SECURITY: Reject malformed query parameters
88
+ return null;
89
+ }
90
+ }
91
+ // For static templates (no parameters allowed), reject any query string
92
+ if (this.queryParams.length === 0 && queryString.trim().length > 0) {
93
+ return null;
94
+ }
95
+ return extractedVars;
96
+ }
97
+ /**
98
+ * Expands the template with given variables
99
+ * @param variables Object containing variable values
100
+ * @returns Expanded URI string
101
+ */
102
+ expand(variables) {
103
+ if (this.queryParams.length === 0) {
104
+ return this.baseUri;
105
+ }
106
+ const queryPairs = [];
107
+ for (const param of this.queryParams) {
108
+ const value = variables[param];
109
+ if (value !== undefined && value !== null && value !== "") {
110
+ queryPairs.push(`${encodeURIComponent(param)}=${encodeURIComponent(value)}`);
111
+ }
112
+ }
113
+ if (queryPairs.length === 0) {
114
+ return this.baseUri;
115
+ }
116
+ return `${this.baseUri}?${queryPairs.join("&")}`;
117
+ }
118
+ /**
119
+ * Gets the variable names from the template
120
+ */
121
+ get variableNames() {
122
+ return [...this.queryParams];
123
+ }
124
+ }
125
+ exports.CustomUriTemplate = CustomUriTemplate;
126
+ /**
127
+ * Custom ResourceTemplate that uses our CustomUriTemplate for proper URI matching
128
+ * Duck-types the MCP SDK's ResourceTemplate interface for compatibility
129
+ */
130
+ class CustomResourceTemplate {
131
+ _uriTemplate;
132
+ _callbacks;
133
+ constructor(uriTemplate, callbacks) {
134
+ this._callbacks = callbacks;
135
+ this._uriTemplate = new CustomUriTemplate(uriTemplate);
136
+ }
137
+ /**
138
+ * Gets the URI template pattern - must match MCP SDK interface
139
+ */
140
+ get uriTemplate() {
141
+ return this._uriTemplate;
142
+ }
143
+ /**
144
+ * Gets the list callback, if one was provided
145
+ */
146
+ get listCallback() {
147
+ return this._callbacks.list;
148
+ }
149
+ /**
150
+ * Gets the callback for completing a specific URI template variable
151
+ */
152
+ completeCallback(variable) {
153
+ return this._callbacks.complete?.[variable];
154
+ }
155
+ }
156
+ exports.CustomResourceTemplate = CustomResourceTemplate;
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ /**
3
+ * Custom URI template implementation that fixes the MCP SDK's broken
4
+ * URI template matching for grouped query parameters.
5
+ *
6
+ * This is duck typing implementation of the ResourceTemplate class.
7
+ * See @modelcontextprotocol/sdk/server/mcp.js
8
+ *
9
+ * This is only a temporary solution, as we should use the official implementation from the SDK
10
+ * Upon the SDK being fixed, we should switch over to that implementation.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.CustomResourceTemplate = exports.CustomUriTemplate = void 0;
14
+ // TODO: Get rid of 'any' typing
15
+ /**
16
+ * Custom URI template class that properly handles grouped query parameters
17
+ * in the format {?param1,param2,param3}
18
+ */
19
+ class CustomUriTemplate {
20
+ template;
21
+ baseUri = "";
22
+ queryParams = [];
23
+ constructor(template) {
24
+ this.template = template;
25
+ this.parseTemplate();
26
+ }
27
+ toString() {
28
+ return this.template;
29
+ }
30
+ parseTemplate() {
31
+ // Extract base URI and query parameters from template
32
+ // Template format: odata://CatalogService/books{?filter,orderby,select,skip,top}
33
+ const queryTemplateMatch = this.template.match(/^([^{]+)\{\?([^}]+)\}$/);
34
+ if (!queryTemplateMatch) {
35
+ // No query parameters, treat as static URI
36
+ this.baseUri = this.template;
37
+ this.queryParams = [];
38
+ return;
39
+ }
40
+ this.baseUri = queryTemplateMatch[1];
41
+ this.queryParams = queryTemplateMatch[2]
42
+ .split(",")
43
+ .map((param) => param.trim())
44
+ .filter((param) => param.length > 0);
45
+ }
46
+ /**
47
+ * Matches a URI against this template and extracts variables
48
+ * @param uri The URI to match
49
+ * @returns Object with extracted variables or null if no match
50
+ */
51
+ match(uri) {
52
+ // Check if base URI matches
53
+ if (!uri.startsWith(this.baseUri)) {
54
+ return null;
55
+ }
56
+ // Extract query string
57
+ const queryStart = uri.indexOf("?");
58
+ if (queryStart === -1) {
59
+ // No query parameters in URI
60
+ if (this.queryParams.length === 0) {
61
+ return {}; // Static URI match
62
+ }
63
+ // Template expects query params but URI has none - still valid for optional params
64
+ return {};
65
+ }
66
+ const queryString = uri.substring(queryStart + 1);
67
+ const queryPairs = queryString.split("&");
68
+ const extractedVars = {};
69
+ // Parse query parameters with strict validation
70
+ for (const pair of queryPairs) {
71
+ const equalIndex = pair.indexOf("=");
72
+ if (equalIndex > 0) {
73
+ const key = pair.substring(0, equalIndex);
74
+ const value = pair.substring(equalIndex + 1);
75
+ if (key && value !== undefined) {
76
+ const decodedKey = decodeURIComponent(key);
77
+ const decodedValue = decodeURIComponent(value);
78
+ // SECURITY: Reject entire URI if ANY unauthorized parameter is present
79
+ if (!this.queryParams.includes(decodedKey)) {
80
+ return null; // Unauthorized parameter found - reject entire URI
81
+ }
82
+ extractedVars[decodedKey] = decodedValue;
83
+ }
84
+ }
85
+ else if (pair.trim().length > 0) {
86
+ // Handle malformed parameters (missing = or empty key)
87
+ // SECURITY: Reject malformed query parameters
88
+ return null;
89
+ }
90
+ }
91
+ // For static templates (no parameters allowed), reject any query string
92
+ if (this.queryParams.length === 0 && queryString.trim().length > 0) {
93
+ return null;
94
+ }
95
+ return extractedVars;
96
+ }
97
+ /**
98
+ * Expands the template with given variables
99
+ * @param variables Object containing variable values
100
+ * @returns Expanded URI string
101
+ */
102
+ expand(variables) {
103
+ if (this.queryParams.length === 0) {
104
+ return this.baseUri;
105
+ }
106
+ const queryPairs = [];
107
+ for (const param of this.queryParams) {
108
+ const value = variables[param];
109
+ if (value !== undefined && value !== null && value !== "") {
110
+ queryPairs.push(`${encodeURIComponent(param)}=${encodeURIComponent(value)}`);
111
+ }
112
+ }
113
+ if (queryPairs.length === 0) {
114
+ return this.baseUri;
115
+ }
116
+ return `${this.baseUri}?${queryPairs.join("&")}`;
117
+ }
118
+ /**
119
+ * Gets the variable names from the template
120
+ */
121
+ get variableNames() {
122
+ return [...this.queryParams];
123
+ }
124
+ }
125
+ exports.CustomUriTemplate = CustomUriTemplate;
126
+ /**
127
+ * Custom ResourceTemplate that uses our CustomUriTemplate for proper URI matching
128
+ * Duck-types the MCP SDK's ResourceTemplate interface for compatibility
129
+ */
130
+ class CustomResourceTemplate {
131
+ _uriTemplate;
132
+ _callbacks;
133
+ constructor(uriTemplate, callbacks) {
134
+ this._callbacks = callbacks;
135
+ this._uriTemplate = new CustomUriTemplate(uriTemplate);
136
+ }
137
+ /**
138
+ * Gets the URI template pattern - must match MCP SDK interface
139
+ */
140
+ get uriTemplate() {
141
+ return this._uriTemplate;
142
+ }
143
+ /**
144
+ * Gets the list callback, if one was provided
145
+ */
146
+ get listCallback() {
147
+ return this._callbacks.list;
148
+ }
149
+ /**
150
+ * Gets the callback for completing a specific URI template variable
151
+ */
152
+ completeCallback(variable) {
153
+ return this._callbacks.complete?.[variable];
154
+ }
155
+ }
156
+ exports.CustomResourceTemplate = CustomResourceTemplate;
@@ -7,6 +7,12 @@ const structures_1 = require("../annotations/structures");
7
7
  const tools_1 = require("./tools");
8
8
  const resources_1 = require("./resources");
9
9
  const prompts_1 = require("./prompts");
10
+ /**
11
+ * Creates and configures an MCP server instance with the given configuration and annotations
12
+ * @param config - CAP configuration object
13
+ * @param annotations - Optional parsed annotations to register with the server
14
+ * @returns Configured MCP server instance
15
+ */
10
16
  function createMcpServer(config, annotations) {
11
17
  logger_1.LOGGER.debug("Creating MCP server instance");
12
18
  const server = new mcp_js_1.McpServer({
@@ -14,28 +20,26 @@ function createMcpServer(config, annotations) {
14
20
  version: config.version,
15
21
  capabilities: config.capabilities,
16
22
  });
17
- if (!annotations)
23
+ if (!annotations) {
24
+ logger_1.LOGGER.debug("No annotations provided, skipping registration...");
18
25
  return server;
26
+ }
19
27
  logger_1.LOGGER.debug("Annotations found for server: ", annotations);
20
- // TODO: Handle the parsed annotations
21
- // TODO: Error handling
22
- // TODO: This should only be mapped once, not per each server instance. Maybe this should be pre-packaged on load?
23
28
  // TODO: Handle auth
24
29
  for (const entry of annotations.values()) {
25
- switch (entry.constructor) {
26
- case structures_1.McpToolAnnotation:
27
- (0, tools_1.assignToolToServer)(entry, server);
28
- continue;
29
- case structures_1.McpResourceAnnotation:
30
- (0, resources_1.assignResourceToServer)(entry, server);
31
- continue;
32
- case structures_1.McpPromptAnnotation:
33
- (0, prompts_1.assignPromptToServer)(entry, server);
34
- continue;
35
- default:
36
- logger_1.LOGGER.warn("Invalid annotation entry - Cannot be parsed by MCP server, skipping...");
37
- continue;
30
+ if (entry instanceof structures_1.McpToolAnnotation) {
31
+ (0, tools_1.assignToolToServer)(entry, server);
32
+ continue;
33
+ }
34
+ else if (entry instanceof structures_1.McpResourceAnnotation) {
35
+ (0, resources_1.assignResourceToServer)(entry, server);
36
+ continue;
37
+ }
38
+ else if (entry instanceof structures_1.McpPromptAnnotation) {
39
+ (0, prompts_1.assignPromptToServer)(entry, server);
40
+ continue;
38
41
  }
42
+ logger_1.LOGGER.warn("Invalid annotation entry - Cannot be parsed by MCP server, skipping...");
39
43
  }
40
44
  return server;
41
45
  }
@@ -3,8 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.assignPromptToServer = assignPromptToServer;
4
4
  const logger_1 = require("../logger");
5
5
  const utils_1 = require("./utils");
6
- /* @ts-ignore */
7
- const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
8
6
  // NOTE: Not satisfied with below implementation, will need to be revised for full effect
9
7
  /*
10
8
  annotate CatalogService with @mcp.prompts: [{
@@ -18,6 +16,12 @@ annotate CatalogService with @mcp.prompts: [{
18
16
  }]
19
17
  }];
20
18
  */
19
+ /**
20
+ * Registers prompt templates from a prompt annotation with the MCP server
21
+ * Each prompt template supports variable substitution using {{variable}} syntax
22
+ * @param model - The prompt annotation containing template definitions and inputs
23
+ * @param server - The MCP server instance to register prompts with
24
+ */
21
25
  function assignPromptToServer(model, server) {
22
26
  logger_1.LOGGER.debug("Adding prompt", model);
23
27
  for (const prompt of model.prompts) {
@@ -45,6 +49,12 @@ function assignPromptToServer(model, server) {
45
49
  });
46
50
  }
47
51
  }
52
+ /**
53
+ * Builds Zod schema definitions for prompt input parameters
54
+ * Converts CDS type strings to appropriate Zod validation schemas
55
+ * @param inputs - Array of prompt input parameter definitions
56
+ * @returns Record mapping parameter names to Zod schemas, or undefined if no inputs
57
+ */
48
58
  function constructInputArgs(inputs) {
49
59
  // Not happy with using any here, but zod types are hard to figure out....
50
60
  if (!inputs || inputs.length <= 0)
@@ -1,45 +1,87 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.assignResourceToServer = assignResourceToServer;
4
- const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
4
+ const custom_resource_template_1 = require("./custom-resource-template");
5
5
  const logger_1 = require("../logger");
6
6
  const utils_1 = require("./utils");
7
+ const validation_1 = require("./validation");
7
8
  // import cds from "@sap/cds";
8
9
  /* @ts-ignore */
9
10
  const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
11
+ /**
12
+ * Registers a CAP entity as an MCP resource with optional OData query support
13
+ * Creates either static or dynamic resources based on configured functionalities
14
+ * @param model - The resource annotation containing entity metadata and query options
15
+ * @param server - The MCP server instance to register the resource with
16
+ */
10
17
  function assignResourceToServer(model, server) {
11
18
  logger_1.LOGGER.debug("Adding resource", model);
12
19
  if (model.functionalities.size <= 0) {
13
20
  registerStaticResource(model, server);
21
+ return;
14
22
  }
15
23
  // Dynamic resource registration
16
24
  const detailedDescription = (0, utils_1.writeODataDescriptionForResource)(model);
17
- const functionalities = Array.from(model.functionalities).map((el) => `{?${el}}`);
18
- // BUG: RFC compliance breaking bug in the MCP SDK library, must wait for fix....
19
- const resourceTemplateUri = `odata://${model.serviceName}/${model.name}${functionalities.join("")}`;
20
- const template = new mcp_js_1.ResourceTemplate(resourceTemplateUri, {
25
+ const functionalities = Array.from(model.functionalities);
26
+ // Using grouped query parameter format to fix MCP SDK URI matching issue
27
+ // Format: {?param1,param2,param3} instead of {?param1}{?param2}{?param3}
28
+ const templateParams = functionalities.length > 0 ? `{?${functionalities.join(",")}}` : "";
29
+ const resourceTemplateUri = `odata://${model.serviceName}/${model.name}${templateParams}`;
30
+ const template = new custom_resource_template_1.CustomResourceTemplate(resourceTemplateUri, {
21
31
  list: undefined,
22
32
  });
23
- server.registerResource(model.name, template, { title: model.target, description: detailedDescription }, async (uri, queryParameters) => {
33
+ server.registerResource(model.name, template, // Type assertion to bypass strict type checking - necessary due to broken URI parser in the MCP SDK
34
+ { title: model.target, description: detailedDescription }, async (uri, variables) => {
35
+ const queryParameters = variables;
24
36
  const service = cds.services[model.serviceName];
25
- const query = SELECT.from(model.target).limit(queryParameters.top ? Number(queryParameters.top) : 100, queryParameters.skip ? Number(queryParameters.skip) : undefined);
26
- for (const [k, v] of Object.entries(queryParameters)) {
27
- switch (k) {
28
- case "filter":
29
- const expression = cds.parse.expr(decodeURIComponent(v));
30
- query.where(expression);
31
- continue;
32
- case "select":
33
- const decodedSelect = decodeURIComponent(v);
34
- query.columns(decodedSelect.split(","));
35
- continue;
36
- case "orderby":
37
- query.orderBy(decodeURIComponent(v));
38
- continue;
39
- default:
37
+ if (!service) {
38
+ logger_1.LOGGER.error(`Invalid service found for service '${model.serviceName}'`);
39
+ throw new Error(`Invalid service found for service '${model.serviceName}'`);
40
+ }
41
+ // Create validator with entity properties
42
+ const validator = new validation_1.ODataQueryValidator(model.properties);
43
+ // Validate and build query with secure parameter handling
44
+ let query;
45
+ try {
46
+ query = SELECT.from(model.target).limit(queryParameters.top
47
+ ? validator.validateTop(queryParameters.top)
48
+ : 100, queryParameters.skip
49
+ ? validator.validateSkip(queryParameters.skip)
50
+ : undefined);
51
+ for (const [k, v] of Object.entries(queryParameters)) {
52
+ if (!v || v.trim().length <= 0)
40
53
  continue;
54
+ switch (k) {
55
+ case "filter":
56
+ // BUG: If filter value is e.g. "filter=1234" the value 1234 will go through
57
+ const validatedFilter = validator.validateFilter(v);
58
+ const expression = cds.parse.expr(validatedFilter);
59
+ query.where(expression);
60
+ continue;
61
+ case "select":
62
+ const validatedColumns = validator.validateSelect(v);
63
+ query.columns(validatedColumns);
64
+ continue;
65
+ case "orderby":
66
+ const validatedOrderBy = validator.validateOrderBy(v);
67
+ query.orderBy(validatedOrderBy);
68
+ continue;
69
+ default:
70
+ continue;
71
+ }
41
72
  }
42
73
  }
74
+ catch (error) {
75
+ logger_1.LOGGER.warn(`OData query validation failed for ${model.target}:`, error);
76
+ return {
77
+ contents: [
78
+ {
79
+ uri: uri.href,
80
+ text: `ERROR: Invalid query parameter - ${error instanceof validation_1.ODataValidationError ? error.message : "Invalid query syntax"}`,
81
+ },
82
+ ],
83
+ };
84
+ }
43
85
  try {
44
86
  const response = await service.run(query);
45
87
  return {
@@ -64,11 +106,22 @@ function assignResourceToServer(model, server) {
64
106
  }
65
107
  });
66
108
  }
109
+ /**
110
+ * Registers a static resource without OData query functionality
111
+ * Used when no query functionalities are configured for the resource
112
+ * @param model - The resource annotation with entity metadata
113
+ * @param server - The MCP server instance to register with
114
+ */
67
115
  function registerStaticResource(model, server) {
68
- server.registerResource(model.name, `odata://${model.serviceName}/${model.name}`, { title: model.target, description: model.description }, async (uri, queryParameters) => {
116
+ server.registerResource(model.name, `odata://${model.serviceName}/${model.name}`, { title: model.target, description: model.description }, async (uri, extra) => {
117
+ const queryParameters = extra;
69
118
  const service = cds.services[model.serviceName];
70
- const query = SELECT.from(model.target).limit(queryParameters.top ? Number(queryParameters.top) : 100);
119
+ // Create validator even for static resources to validate top parameter
120
+ const validator = new validation_1.ODataQueryValidator(model.properties);
71
121
  try {
122
+ const query = SELECT.from(model.target).limit(queryParameters.top
123
+ ? validator.validateTop(queryParameters.top)
124
+ : 100);
72
125
  const response = await service.run(query);
73
126
  return {
74
127
  contents: [
@@ -79,8 +132,19 @@ function registerStaticResource(model, server) {
79
132
  ],
80
133
  };
81
134
  }
82
- catch (e) {
83
- logger_1.LOGGER.error(`Failed to retrieve resource data for ${model.target}`, e);
135
+ catch (error) {
136
+ if (error instanceof validation_1.ODataValidationError) {
137
+ logger_1.LOGGER.warn(`OData validation failed for static resource ${model.target}:`, error);
138
+ return {
139
+ contents: [
140
+ {
141
+ uri: uri.href,
142
+ text: `ERROR: Invalid query parameter - ${error.message}`,
143
+ },
144
+ ],
145
+ };
146
+ }
147
+ logger_1.LOGGER.error(`Failed to retrieve resource data for ${model.target}`, error);
84
148
  return {
85
149
  contents: [
86
150
  {