@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.
- package/lib/annotations/constants.js +22 -4
- package/lib/annotations/parser.js +41 -3
- package/lib/annotations/structures.js +111 -0
- package/lib/annotations/utils.js +55 -0
- package/lib/config/env-sanitizer.js +78 -0
- package/lib/config/json-parser.js +165 -0
- package/lib/config/loader.js +20 -12
- package/lib/logger.js +8 -0
- package/lib/mcp/constants.js +16 -0
- package/lib/mcp/custom-resource-template.js +156 -0
- package/lib/mcp/customResourceTemplate.js +156 -0
- package/lib/mcp/factory.js +21 -17
- package/lib/mcp/prompts.js +12 -2
- package/lib/mcp/resources.js +89 -25
- package/lib/mcp/session-manager.js +92 -0
- package/lib/mcp/tools.js +51 -9
- package/lib/mcp/utils.js +12 -3
- package/lib/mcp/validation.js +318 -0
- package/lib/mcp.js +70 -42
- package/package.json +18 -6
@@ -1,18 +1,36 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.DEFAULT_ALL_RESOURCE_OPTIONS = exports.MCP_ANNOTATION_PROPS = exports.MCP_ANNOTATION_KEY = void 0;
|
4
|
+
/**
|
5
|
+
* MCP annotation constants and default configurations
|
6
|
+
* Defines the standard annotation keys and default values used throughout the plugin
|
7
|
+
*/
|
8
|
+
/**
|
9
|
+
* Base key used to identify MCP annotations in CDS definitions
|
10
|
+
* All MCP annotations must start with this prefix
|
11
|
+
*/
|
4
12
|
exports.MCP_ANNOTATION_KEY = "@mcp";
|
13
|
+
/**
|
14
|
+
* Complete set of supported MCP annotation property names
|
15
|
+
* Maps logical names to their actual annotation keys used in CDS files
|
16
|
+
*/
|
5
17
|
exports.MCP_ANNOTATION_PROPS = {
|
6
|
-
|
18
|
+
/** Name identifier annotation - required for all MCP elements */
|
7
19
|
MCP_NAME: "@mcp.name",
|
20
|
+
/** Description annotation - required for all MCP elements */
|
8
21
|
MCP_DESCRIPTION: "@mcp.description",
|
9
|
-
|
22
|
+
/** Resource configuration annotation for CAP entities */
|
10
23
|
MCP_RESOURCE: "@mcp.resource",
|
11
|
-
|
24
|
+
/** Tool configuration annotation for CAP functions/actions */
|
12
25
|
MCP_TOOL: "@mcp.tool",
|
13
|
-
|
26
|
+
/** Prompt templates annotation for CAP services */
|
14
27
|
MCP_PROMPT: "@mcp.prompts",
|
15
28
|
};
|
29
|
+
/**
|
30
|
+
* Default set of all available OData query options for MCP resources
|
31
|
+
* Used when @mcp.resource is set to `true` to enable all capabilities
|
32
|
+
* Includes: $filter, $orderby, $top, $skip, $select
|
33
|
+
*/
|
16
34
|
exports.DEFAULT_ALL_RESOURCE_OPTIONS = new Set([
|
17
35
|
"filter",
|
18
36
|
"orderby",
|
@@ -5,6 +5,12 @@ const logger_1 = require("../logger");
|
|
5
5
|
const structures_1 = require("./structures");
|
6
6
|
const utils_1 = require("./utils");
|
7
7
|
const constants_1 = require("./constants");
|
8
|
+
/**
|
9
|
+
* Parses model definitions to extract MCP annotations and return them as a map of annotated entries
|
10
|
+
* @param model - The CSN model containing definitions to parse
|
11
|
+
* @returns A map of target names to their corresponding MCP annotation entries
|
12
|
+
* @throws Error if model lacks valid definitions
|
13
|
+
*/
|
8
14
|
function parseDefinitions(model) {
|
9
15
|
if (!model.definitions) {
|
10
16
|
logger_1.LOGGER.error("Invalid model loaded", model);
|
@@ -50,6 +56,11 @@ function parseDefinitions(model) {
|
|
50
56
|
}
|
51
57
|
return result;
|
52
58
|
}
|
59
|
+
/**
|
60
|
+
* Parses MCP annotations from a definition object
|
61
|
+
* @param definition - The definition object to parse annotations from
|
62
|
+
* @returns Partial annotation structure or undefined if no MCP annotations found
|
63
|
+
*/
|
53
64
|
function parseAnnotations(definition) {
|
54
65
|
if (!(0, utils_1.containsMcpAnnotation)(definition))
|
55
66
|
return undefined;
|
@@ -82,6 +93,14 @@ function parseAnnotations(definition) {
|
|
82
93
|
}
|
83
94
|
return annotations;
|
84
95
|
}
|
96
|
+
/**
|
97
|
+
* Constructs a resource annotation from parsed annotation data
|
98
|
+
* @param serviceName - Name of the service containing the resource
|
99
|
+
* @param target - Target entity name
|
100
|
+
* @param annotations - Parsed annotation structure
|
101
|
+
* @param definition - CSN definition object
|
102
|
+
* @returns Resource annotation or undefined if invalid
|
103
|
+
*/
|
85
104
|
function constructResourceAnnotation(serviceName, target, annotations, definition) {
|
86
105
|
if (!(0, utils_1.isValidResourceAnnotation)(annotations))
|
87
106
|
return undefined;
|
@@ -89,26 +108,45 @@ function constructResourceAnnotation(serviceName, target, annotations, definitio
|
|
89
108
|
const { properties, resourceKeys } = (0, utils_1.parseResourceElements)(definition);
|
90
109
|
return new structures_1.McpResourceAnnotation(annotations.name, annotations.description, target, serviceName, functionalities, properties, resourceKeys);
|
91
110
|
}
|
111
|
+
/**
|
112
|
+
* Constructs a tool annotation from parsed annotation data
|
113
|
+
* @param serviceName - Name of the service containing the tool
|
114
|
+
* @param target - Target operation name
|
115
|
+
* @param annotations - Parsed annotation structure
|
116
|
+
* @param entityKey - Optional entity key for bound operations
|
117
|
+
* @param keyParams - Optional key parameters for bound operations
|
118
|
+
* @returns Tool annotation or undefined if invalid
|
119
|
+
*/
|
92
120
|
function constructToolAnnotation(serviceName, target, annotations, entityKey, keyParams) {
|
93
121
|
if (!(0, utils_1.isValidToolAnnotation)(annotations))
|
94
122
|
return undefined;
|
95
123
|
const { parameters, operationKind } = (0, utils_1.parseOperationElements)(annotations);
|
96
124
|
return new structures_1.McpToolAnnotation(annotations.name, annotations.description, target, serviceName, parameters, entityKey, operationKind, keyParams);
|
97
125
|
}
|
126
|
+
/**
|
127
|
+
* Constructs a prompt annotation from parsed annotation data
|
128
|
+
* @param serviceName - Name of the service containing the prompts
|
129
|
+
* @param annotations - Parsed annotation structure
|
130
|
+
* @returns Prompt annotation or undefined if invalid
|
131
|
+
*/
|
98
132
|
function constructPromptAnnotation(serviceName, annotations) {
|
99
133
|
if (!(0, utils_1.isValidPromptsAnnotation)(annotations))
|
100
134
|
return undefined;
|
101
135
|
return new structures_1.McpPromptAnnotation(annotations.name, annotations.description, serviceName, annotations.prompts);
|
102
136
|
}
|
103
137
|
/**
|
104
|
-
* Parses
|
105
|
-
*
|
138
|
+
* Parses bound operations (actions/functions) attached to an entity definition
|
139
|
+
* Extracts MCP tool annotations from entity-level operations and adds them to the result map
|
140
|
+
* @param serviceName - Name of the service containing the entity
|
141
|
+
* @param entityKey - Name of the entity that owns these bound operations
|
142
|
+
* @param definition - CSN entity definition containing bound operations
|
143
|
+
* @param resultRef - Map to store parsed annotations (mutated by this function)
|
106
144
|
*/
|
107
145
|
function parseBoundOperations(serviceName, entityKey, definition, resultRef) {
|
108
146
|
if (definition.kind !== "entity")
|
109
147
|
return;
|
110
148
|
const boundOperations = definition
|
111
|
-
.actions; // Necessary due to missing type reference
|
149
|
+
.actions; // NOTE: Necessary due to missing type reference in cds-types
|
112
150
|
if (!boundOperations)
|
113
151
|
return;
|
114
152
|
const keyParams = (0, utils_1.parseEntityKeys)(definition);
|
@@ -1,57 +1,136 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.McpPromptAnnotation = exports.McpToolAnnotation = exports.McpResourceAnnotation = exports.McpAnnotation = void 0;
|
4
|
+
/**
|
5
|
+
* Base class for all MCP annotations that provides common properties
|
6
|
+
* and functionality shared across different annotation types
|
7
|
+
*/
|
4
8
|
class McpAnnotation {
|
9
|
+
/** The name identifier for this annotation */
|
5
10
|
_name;
|
11
|
+
/** AI agent readable description of what this annotation represents */
|
6
12
|
_description;
|
13
|
+
/** The target entity, function, or service element this annotation applies to */
|
7
14
|
_target;
|
15
|
+
/** The name of the CAP service this annotation belongs to */
|
8
16
|
_serviceName;
|
17
|
+
/**
|
18
|
+
* Creates a new MCP annotation instance
|
19
|
+
* @param name - Unique identifier for this annotation
|
20
|
+
* @param description - Human-readable description
|
21
|
+
* @param target - The target element this annotation applies to
|
22
|
+
* @param serviceName - Name of the associated CAP service
|
23
|
+
*/
|
9
24
|
constructor(name, description, target, serviceName) {
|
10
25
|
this._name = name;
|
11
26
|
this._description = description;
|
12
27
|
this._target = target;
|
13
28
|
this._serviceName = serviceName;
|
14
29
|
}
|
30
|
+
/**
|
31
|
+
* Gets the unique name identifier for this annotation
|
32
|
+
* @returns The annotation name
|
33
|
+
*/
|
15
34
|
get name() {
|
16
35
|
return this._name;
|
17
36
|
}
|
37
|
+
/**
|
38
|
+
* Gets the human-readable description of this annotation
|
39
|
+
* @returns The annotation description
|
40
|
+
*/
|
18
41
|
get description() {
|
19
42
|
return this._description;
|
20
43
|
}
|
44
|
+
/**
|
45
|
+
* Gets the target element this annotation applies to
|
46
|
+
* @returns The target identifier
|
47
|
+
*/
|
21
48
|
get target() {
|
22
49
|
return this._target;
|
23
50
|
}
|
51
|
+
/**
|
52
|
+
* Gets the name of the CAP service this annotation belongs to
|
53
|
+
* @returns The service name
|
54
|
+
*/
|
24
55
|
get serviceName() {
|
25
56
|
return this._serviceName;
|
26
57
|
}
|
27
58
|
}
|
28
59
|
exports.McpAnnotation = McpAnnotation;
|
60
|
+
/**
|
61
|
+
* Annotation class for MCP resources that can be queried with OData parameters
|
62
|
+
* Extends the base annotation with resource-specific configuration
|
63
|
+
*/
|
29
64
|
class McpResourceAnnotation extends McpAnnotation {
|
65
|
+
/** Set of OData query functionalities enabled for this resource */
|
30
66
|
_functionalities;
|
67
|
+
/** Map of property names to their CDS types for validation */
|
31
68
|
_properties;
|
69
|
+
/** Map of resource key fields to their types */
|
32
70
|
_resourceKeys;
|
71
|
+
/**
|
72
|
+
* Creates a new MCP resource annotation
|
73
|
+
* @param name - Unique identifier for this resource
|
74
|
+
* @param description - Human-readable description
|
75
|
+
* @param target - The CAP entity this resource represents
|
76
|
+
* @param serviceName - Name of the associated CAP service
|
77
|
+
* @param functionalities - Set of enabled OData query options (filter, top, skip, etc.)
|
78
|
+
* @param properties - Map of entity properties to their CDS types
|
79
|
+
* @param resourceKeys - Map of key fields to their types
|
80
|
+
*/
|
33
81
|
constructor(name, description, target, serviceName, functionalities, properties, resourceKeys) {
|
34
82
|
super(name, description, target, serviceName);
|
35
83
|
this._functionalities = functionalities;
|
36
84
|
this._properties = properties;
|
37
85
|
this._resourceKeys = resourceKeys;
|
38
86
|
}
|
87
|
+
/**
|
88
|
+
* Gets the set of enabled OData query functionalities
|
89
|
+
* @returns Set of available query options like 'filter', 'top', 'skip'
|
90
|
+
*/
|
39
91
|
get functionalities() {
|
40
92
|
return this._functionalities;
|
41
93
|
}
|
94
|
+
/**
|
95
|
+
* Gets the map of entity properties to their CDS types
|
96
|
+
* @returns Map of property names to type strings
|
97
|
+
*/
|
42
98
|
get properties() {
|
43
99
|
return this._properties;
|
44
100
|
}
|
101
|
+
/**
|
102
|
+
* Gets the map of resource key fields to their types
|
103
|
+
* @returns Map of key field names to type strings
|
104
|
+
*/
|
45
105
|
get resourceKeys() {
|
46
106
|
return this._resourceKeys;
|
47
107
|
}
|
48
108
|
}
|
49
109
|
exports.McpResourceAnnotation = McpResourceAnnotation;
|
110
|
+
/**
|
111
|
+
* Annotation class for MCP tools that represent executable CAP functions or actions
|
112
|
+
* Can be either bound (entity-level) or unbound (service-level) operations
|
113
|
+
*/
|
50
114
|
class McpToolAnnotation extends McpAnnotation {
|
115
|
+
/** Map of function parameters to their CDS types */
|
51
116
|
_parameters;
|
117
|
+
/** Entity key field name for bound operations */
|
52
118
|
_entityKey;
|
119
|
+
/** Type of operation: 'function' or 'action' */
|
53
120
|
_operationKind;
|
121
|
+
/** Map of key field names to their types for bound operations */
|
54
122
|
_keyTypeMap;
|
123
|
+
/**
|
124
|
+
* Creates a new MCP tool annotation
|
125
|
+
* @param name - Unique identifier for this tool
|
126
|
+
* @param description - Human-readable description
|
127
|
+
* @param operation - The CAP function or action name
|
128
|
+
* @param serviceName - Name of the associated CAP service
|
129
|
+
* @param parameters - Optional map of function parameters to their types
|
130
|
+
* @param entityKey - Optional entity key field for bound operations
|
131
|
+
* @param operationKind - Optional operation type ('function' or 'action')
|
132
|
+
* @param keyTypeMap - Optional map of key fields to types for bound operations
|
133
|
+
*/
|
55
134
|
constructor(name, description, operation, serviceName, parameters, entityKey, operationKind, keyTypeMap) {
|
56
135
|
super(name, description, operation, serviceName);
|
57
136
|
this._parameters = parameters;
|
@@ -59,26 +138,58 @@ class McpToolAnnotation extends McpAnnotation {
|
|
59
138
|
this._operationKind = operationKind;
|
60
139
|
this._keyTypeMap = keyTypeMap;
|
61
140
|
}
|
141
|
+
/**
|
142
|
+
* Gets the map of function parameters to their CDS types
|
143
|
+
* @returns Map of parameter names to type strings, or undefined if no parameters
|
144
|
+
*/
|
62
145
|
get parameters() {
|
63
146
|
return this._parameters;
|
64
147
|
}
|
148
|
+
/**
|
149
|
+
* Gets the entity key field name for bound operations
|
150
|
+
* @returns Entity key field name, or undefined for unbound operations
|
151
|
+
*/
|
65
152
|
get entityKey() {
|
66
153
|
return this._entityKey;
|
67
154
|
}
|
155
|
+
/**
|
156
|
+
* Gets the operation kind (function or action)
|
157
|
+
* @returns Operation type string, or undefined if not specified
|
158
|
+
*/
|
68
159
|
get operationKind() {
|
69
160
|
return this._operationKind;
|
70
161
|
}
|
162
|
+
/**
|
163
|
+
* Gets the map of key field names to their types for bound operations
|
164
|
+
* @returns Map of key fields to types, or undefined for unbound operations
|
165
|
+
*/
|
71
166
|
get keyTypeMap() {
|
72
167
|
return this._keyTypeMap;
|
73
168
|
}
|
74
169
|
}
|
75
170
|
exports.McpToolAnnotation = McpToolAnnotation;
|
171
|
+
/**
|
172
|
+
* Annotation class for MCP prompts that define reusable prompt templates
|
173
|
+
* Applied at the service level to provide prompt templates with variable substitution
|
174
|
+
*/
|
76
175
|
class McpPromptAnnotation extends McpAnnotation {
|
176
|
+
/** Array of prompt template definitions */
|
77
177
|
_prompts;
|
178
|
+
/**
|
179
|
+
* Creates a new MCP prompt annotation
|
180
|
+
* @param name - Unique identifier for this prompt collection
|
181
|
+
* @param description - Human-readable description
|
182
|
+
* @param serviceName - Name of the associated CAP service
|
183
|
+
* @param prompts - Array of prompt template definitions
|
184
|
+
*/
|
78
185
|
constructor(name, description, serviceName, prompts) {
|
79
186
|
super(name, description, serviceName, serviceName);
|
80
187
|
this._prompts = prompts;
|
81
188
|
}
|
189
|
+
/**
|
190
|
+
* Gets the array of prompt template definitions
|
191
|
+
* @returns Array of prompt templates with their inputs and templates
|
192
|
+
*/
|
82
193
|
get prompts() {
|
83
194
|
return this._prompts;
|
84
195
|
}
|
package/lib/annotations/utils.js
CHANGED
@@ -12,6 +12,11 @@ exports.parseOperationElements = parseOperationElements;
|
|
12
12
|
exports.parseEntityKeys = parseEntityKeys;
|
13
13
|
const constants_1 = require("./constants");
|
14
14
|
const logger_1 = require("../logger");
|
15
|
+
/**
|
16
|
+
* Splits a definition name into service name and target
|
17
|
+
* @param definition - The definition name to split
|
18
|
+
* @returns Object containing serviceName and target
|
19
|
+
*/
|
15
20
|
function splitDefinitionName(definition) {
|
16
21
|
const splitted = definition.split(".");
|
17
22
|
return {
|
@@ -19,6 +24,11 @@ function splitDefinitionName(definition) {
|
|
19
24
|
target: splitted[1],
|
20
25
|
};
|
21
26
|
}
|
27
|
+
/**
|
28
|
+
* Checks if a definition contains any MCP annotations
|
29
|
+
* @param definition - The definition to check
|
30
|
+
* @returns True if MCP annotations are found, false otherwise
|
31
|
+
*/
|
22
32
|
function containsMcpAnnotation(definition) {
|
23
33
|
for (const key of Object.keys(definition)) {
|
24
34
|
if (!key.includes(constants_1.MCP_ANNOTATION_KEY))
|
@@ -27,6 +37,12 @@ function containsMcpAnnotation(definition) {
|
|
27
37
|
}
|
28
38
|
return false;
|
29
39
|
}
|
40
|
+
/**
|
41
|
+
* Validates that required MCP annotations are present and valid
|
42
|
+
* @param annotations - The annotation structure to validate
|
43
|
+
* @returns True if valid, throws error if invalid
|
44
|
+
* @throws Error if required annotations are missing
|
45
|
+
*/
|
30
46
|
function containsRequiredAnnotations(annotations) {
|
31
47
|
if (annotations.definition?.kind === "service")
|
32
48
|
return true;
|
@@ -38,6 +54,12 @@ function containsRequiredAnnotations(annotations) {
|
|
38
54
|
}
|
39
55
|
return true;
|
40
56
|
}
|
57
|
+
/**
|
58
|
+
* Validates a resource annotation structure
|
59
|
+
* @param annotations - The annotation structure to validate
|
60
|
+
* @returns True if valid, throws error if invalid
|
61
|
+
* @throws Error if resource annotation is invalid
|
62
|
+
*/
|
41
63
|
function isValidResourceAnnotation(annotations) {
|
42
64
|
if (!annotations?.resource) {
|
43
65
|
throw new Error(`Invalid annotation '${annotations.definition?.target}' - Missing required flag 'resource'`);
|
@@ -51,12 +73,24 @@ function isValidResourceAnnotation(annotations) {
|
|
51
73
|
}
|
52
74
|
return true;
|
53
75
|
}
|
76
|
+
/**
|
77
|
+
* Validates a tool annotation structure
|
78
|
+
* @param annotations - The annotation structure to validate
|
79
|
+
* @returns True if valid, throws error if invalid
|
80
|
+
* @throws Error if tool annotation is invalid
|
81
|
+
*/
|
54
82
|
function isValidToolAnnotation(annotations) {
|
55
83
|
if (!annotations?.tool) {
|
56
84
|
throw new Error(`Invalid annotation '${annotations.definition?.target}' - Missing required flag 'tool'`);
|
57
85
|
}
|
58
86
|
return true;
|
59
87
|
}
|
88
|
+
/**
|
89
|
+
* Validates a prompts annotation structure
|
90
|
+
* @param annotations - The annotation structure to validate
|
91
|
+
* @returns True if valid, throws error if invalid
|
92
|
+
* @throws Error if prompts annotation is invalid
|
93
|
+
*/
|
60
94
|
function isValidPromptsAnnotation(annotations) {
|
61
95
|
if (!annotations?.prompts) {
|
62
96
|
throw new Error(`Invalid annotation '${annotations.definition?.target}' - Missing prompts annotations`);
|
@@ -87,11 +121,21 @@ function isValidPromptsAnnotation(annotations) {
|
|
87
121
|
}
|
88
122
|
return true;
|
89
123
|
}
|
124
|
+
/**
|
125
|
+
* Determines resource options from annotation structure
|
126
|
+
* @param annotations - The annotation structure to process
|
127
|
+
* @returns Set of resource options, defaults to all options if not specified
|
128
|
+
*/
|
90
129
|
function determineResourceOptions(annotations) {
|
91
130
|
if (!Array.isArray(annotations.resource))
|
92
131
|
return constants_1.DEFAULT_ALL_RESOURCE_OPTIONS;
|
93
132
|
return new Set(annotations.resource);
|
94
133
|
}
|
134
|
+
/**
|
135
|
+
* Parses resource elements from a definition to extract properties and keys
|
136
|
+
* @param definition - The definition to parse
|
137
|
+
* @returns Object containing properties and resource keys maps
|
138
|
+
*/
|
95
139
|
function parseResourceElements(definition) {
|
96
140
|
const properties = new Map();
|
97
141
|
const resourceKeys = new Map();
|
@@ -109,6 +153,11 @@ function parseResourceElements(definition) {
|
|
109
153
|
resourceKeys,
|
110
154
|
};
|
111
155
|
}
|
156
|
+
/**
|
157
|
+
* Parses operation elements from annotation structure
|
158
|
+
* @param annotations - The annotation structure to parse
|
159
|
+
* @returns Object containing parameters and operation kind
|
160
|
+
*/
|
112
161
|
function parseOperationElements(annotations) {
|
113
162
|
let parameters;
|
114
163
|
const params = annotations.definition["params"];
|
@@ -123,6 +172,12 @@ function parseOperationElements(annotations) {
|
|
123
172
|
operationKind: annotations.definition.kind,
|
124
173
|
};
|
125
174
|
}
|
175
|
+
/**
|
176
|
+
* Parses entity keys from a definition
|
177
|
+
* @param definition - The definition to parse keys from
|
178
|
+
* @returns Map of key names to their types
|
179
|
+
* @throws Error if invalid key type is found
|
180
|
+
*/
|
126
181
|
function parseEntityKeys(definition) {
|
127
182
|
const result = new Map();
|
128
183
|
for (const [k, v] of Object.entries(definition.elements)) {
|
@@ -0,0 +1,78 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.getSafeEnvVar = getSafeEnvVar;
|
4
|
+
exports.isTestEnvironment = isTestEnvironment;
|
5
|
+
const logger_1 = require("../logger");
|
6
|
+
/**
|
7
|
+
* Dangerous characters that could be used for injection attacks
|
8
|
+
*/
|
9
|
+
const DANGEROUS_CHARS = /[;&|`$()<>!]/g;
|
10
|
+
/**
|
11
|
+
* Validation patterns for specific environment variables
|
12
|
+
*/
|
13
|
+
const VALIDATION_PATTERNS = {
|
14
|
+
npm_package_name: /^[a-zA-Z0-9\-_@/\.]+$/,
|
15
|
+
npm_package_version: /^\d+\.\d+\.\d+(-[a-zA-Z0-9\-\.]+)?(\+[a-zA-Z0-9\-\.]+)?$/,
|
16
|
+
NODE_ENV: /^(development|production|test)$/,
|
17
|
+
};
|
18
|
+
/**
|
19
|
+
* Sanitizes environment variable value by removing dangerous characters
|
20
|
+
* @param value - Raw environment variable value
|
21
|
+
* @param key - Environment variable key for logging
|
22
|
+
* @returns Sanitized value with dangerous characters removed
|
23
|
+
*/
|
24
|
+
function sanitizeValue(value, key) {
|
25
|
+
const originalValue = value;
|
26
|
+
const sanitized = value.replace(DANGEROUS_CHARS, "").trim();
|
27
|
+
if (sanitized !== originalValue) {
|
28
|
+
logger_1.LOGGER.warn(`Environment variable '${key}' contained potentially dangerous characters and was sanitized`);
|
29
|
+
}
|
30
|
+
return sanitized;
|
31
|
+
}
|
32
|
+
/**
|
33
|
+
* Validates environment variable value against expected pattern
|
34
|
+
* @param value - Sanitized environment variable value
|
35
|
+
* @param key - Environment variable key
|
36
|
+
* @returns True if valid, false otherwise
|
37
|
+
*/
|
38
|
+
function validateValue(value, key) {
|
39
|
+
const pattern = VALIDATION_PATTERNS[key];
|
40
|
+
if (!pattern)
|
41
|
+
return true; // No validation pattern defined
|
42
|
+
const isValid = pattern.test(value);
|
43
|
+
if (!isValid) {
|
44
|
+
logger_1.LOGGER.warn(`Environment variable '${key}' value '${value}' does not match expected pattern`);
|
45
|
+
}
|
46
|
+
return isValid;
|
47
|
+
}
|
48
|
+
/**
|
49
|
+
* Safely retrieves and sanitizes an environment variable
|
50
|
+
* @param key - Environment variable key
|
51
|
+
* @param defaultValue - Default value if environment variable is not set or invalid
|
52
|
+
* @returns Sanitized environment variable value or default
|
53
|
+
*/
|
54
|
+
function getSafeEnvVar(key, defaultValue = "") {
|
55
|
+
const rawValue = process.env[key];
|
56
|
+
// Return default if not set or empty
|
57
|
+
if (!rawValue || rawValue.trim().length === 0) {
|
58
|
+
return defaultValue;
|
59
|
+
}
|
60
|
+
// Sanitize the value
|
61
|
+
const sanitized = sanitizeValue(rawValue, key);
|
62
|
+
// Validate if pattern exists
|
63
|
+
if (key in VALIDATION_PATTERNS) {
|
64
|
+
const isValid = validateValue(sanitized, key);
|
65
|
+
if (!isValid) {
|
66
|
+
logger_1.LOGGER.warn(`Using default value for invalid environment variable '${key}'`);
|
67
|
+
return defaultValue;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
return sanitized;
|
71
|
+
}
|
72
|
+
/**
|
73
|
+
* Checks if current environment is test
|
74
|
+
* @returns True if NODE_ENV is 'test'
|
75
|
+
*/
|
76
|
+
function isTestEnvironment() {
|
77
|
+
return getSafeEnvVar("NODE_ENV", "development") === "test";
|
78
|
+
}
|
@@ -0,0 +1,165 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.JsonParseError = exports.JsonParseErrorType = void 0;
|
4
|
+
exports.safeJsonParse = safeJsonParse;
|
5
|
+
exports.parseCAPConfiguration = parseCAPConfiguration;
|
6
|
+
exports.createSafeErrorMessage = createSafeErrorMessage;
|
7
|
+
const zod_1 = require("zod");
|
8
|
+
const logger_1 = require("../logger");
|
9
|
+
/**
|
10
|
+
* Configuration schema for validation
|
11
|
+
*/
|
12
|
+
const CAPConfigurationSchema = zod_1.z.object({
|
13
|
+
name: zod_1.z.string(),
|
14
|
+
version: zod_1.z.string(),
|
15
|
+
capabilities: zod_1.z.object({
|
16
|
+
tools: zod_1.z.object({
|
17
|
+
listChanged: zod_1.z.boolean().optional(),
|
18
|
+
}),
|
19
|
+
resources: zod_1.z.object({
|
20
|
+
listChanged: zod_1.z.boolean().optional(),
|
21
|
+
subscribe: zod_1.z.boolean().optional(),
|
22
|
+
}),
|
23
|
+
prompts: zod_1.z.object({
|
24
|
+
listChanged: zod_1.z.boolean().optional(),
|
25
|
+
}),
|
26
|
+
}),
|
27
|
+
});
|
28
|
+
/**
|
29
|
+
* Error types for JSON parsing failures
|
30
|
+
*/
|
31
|
+
var JsonParseErrorType;
|
32
|
+
(function (JsonParseErrorType) {
|
33
|
+
JsonParseErrorType["INVALID_INPUT"] = "INVALID_INPUT";
|
34
|
+
JsonParseErrorType["PARSE_ERROR"] = "PARSE_ERROR";
|
35
|
+
JsonParseErrorType["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
36
|
+
})(JsonParseErrorType || (exports.JsonParseErrorType = JsonParseErrorType = {}));
|
37
|
+
/**
|
38
|
+
* Custom error class for JSON parsing failures
|
39
|
+
*/
|
40
|
+
class JsonParseError extends Error {
|
41
|
+
type;
|
42
|
+
details;
|
43
|
+
constructor(type, message, details) {
|
44
|
+
super(message);
|
45
|
+
this.type = type;
|
46
|
+
this.details = details;
|
47
|
+
this.name = "JsonParseError";
|
48
|
+
}
|
49
|
+
}
|
50
|
+
exports.JsonParseError = JsonParseError;
|
51
|
+
/**
|
52
|
+
* Validates basic JSON structure without parsing
|
53
|
+
* @param input - JSON string to validate
|
54
|
+
* @returns True if basic structure is valid
|
55
|
+
*/
|
56
|
+
function hasValidJsonStructure(input) {
|
57
|
+
// Check for balanced braces and brackets
|
58
|
+
let braceCount = 0;
|
59
|
+
let bracketCount = 0;
|
60
|
+
let inString = false;
|
61
|
+
let escaped = false;
|
62
|
+
for (let i = 0; i < input.length; i++) {
|
63
|
+
const char = input[i];
|
64
|
+
if (escaped) {
|
65
|
+
escaped = false;
|
66
|
+
continue;
|
67
|
+
}
|
68
|
+
if (char === "\\") {
|
69
|
+
escaped = true;
|
70
|
+
continue;
|
71
|
+
}
|
72
|
+
if (char === '"') {
|
73
|
+
inString = !inString;
|
74
|
+
continue;
|
75
|
+
}
|
76
|
+
if (inString)
|
77
|
+
continue;
|
78
|
+
switch (char) {
|
79
|
+
case "{":
|
80
|
+
braceCount++;
|
81
|
+
break;
|
82
|
+
case "}":
|
83
|
+
braceCount--;
|
84
|
+
break;
|
85
|
+
case "[":
|
86
|
+
bracketCount++;
|
87
|
+
break;
|
88
|
+
case "]":
|
89
|
+
bracketCount--;
|
90
|
+
break;
|
91
|
+
}
|
92
|
+
// Early detection of imbalanced brackets
|
93
|
+
if (braceCount < 0 || bracketCount < 0) {
|
94
|
+
return false;
|
95
|
+
}
|
96
|
+
}
|
97
|
+
return braceCount === 0 && bracketCount === 0 && !inString;
|
98
|
+
}
|
99
|
+
/**
|
100
|
+
* Safely parses JSON with comprehensive security checks
|
101
|
+
* @param input - JSON string to parse
|
102
|
+
* @param schema - Zod schema for validation
|
103
|
+
* @returns Parsed and validated object or null if parsing fails
|
104
|
+
*/
|
105
|
+
function safeJsonParse(input, schema) {
|
106
|
+
try {
|
107
|
+
// Input validation
|
108
|
+
if (typeof input !== "string") {
|
109
|
+
throw new JsonParseError(JsonParseErrorType.INVALID_INPUT, "Input must be a string", `Received: ${typeof input}`);
|
110
|
+
}
|
111
|
+
// Trim whitespace
|
112
|
+
const trimmed = input.trim();
|
113
|
+
if (trimmed.length === 0) {
|
114
|
+
throw new JsonParseError(JsonParseErrorType.INVALID_INPUT, "Input is empty or contains only whitespace");
|
115
|
+
}
|
116
|
+
// Basic structure validation
|
117
|
+
if (!hasValidJsonStructure(trimmed)) {
|
118
|
+
throw new JsonParseError(JsonParseErrorType.PARSE_ERROR, "Invalid JSON structure detected");
|
119
|
+
}
|
120
|
+
// Parse JSON
|
121
|
+
let parsed;
|
122
|
+
try {
|
123
|
+
parsed = JSON.parse(trimmed);
|
124
|
+
}
|
125
|
+
catch (error) {
|
126
|
+
throw new JsonParseError(JsonParseErrorType.PARSE_ERROR, "JSON parsing failed", error instanceof Error ? error.message : "Unknown parse error");
|
127
|
+
}
|
128
|
+
// Schema validation
|
129
|
+
const validationResult = schema.safeParse(parsed);
|
130
|
+
if (!validationResult.success) {
|
131
|
+
throw new JsonParseError(JsonParseErrorType.VALIDATION_ERROR, "JSON does not match expected schema", validationResult.error.message);
|
132
|
+
}
|
133
|
+
return validationResult.data;
|
134
|
+
}
|
135
|
+
catch (error) {
|
136
|
+
if (error instanceof JsonParseError) {
|
137
|
+
// Log detailed error for debugging (without exposing to user)
|
138
|
+
logger_1.LOGGER.warn(`Safe JSON parsing failed: ${error.type}`, {
|
139
|
+
message: error.message,
|
140
|
+
details: error.details,
|
141
|
+
});
|
142
|
+
}
|
143
|
+
else {
|
144
|
+
// Unexpected error
|
145
|
+
logger_1.LOGGER.warn("Unexpected error during JSON parsing", error);
|
146
|
+
}
|
147
|
+
return null;
|
148
|
+
}
|
149
|
+
}
|
150
|
+
/**
|
151
|
+
* Safely parses CAP configuration JSON
|
152
|
+
* @param input - JSON string containing CAP configuration
|
153
|
+
* @returns Parsed CAPConfiguration or null if parsing fails
|
154
|
+
*/
|
155
|
+
function parseCAPConfiguration(input) {
|
156
|
+
return safeJsonParse(input, CAPConfigurationSchema);
|
157
|
+
}
|
158
|
+
/**
|
159
|
+
* Creates a generic error message for user-facing errors
|
160
|
+
* @param context - Context where the error occurred
|
161
|
+
* @returns Generic error message that doesn't expose sensitive information
|
162
|
+
*/
|
163
|
+
function createSafeErrorMessage(context) {
|
164
|
+
return `Configuration parsing failed in ${context}. Please check the configuration format.`;
|
165
|
+
}
|