mcp-ts-template 1.1.6
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/LICENSE +201 -0
- package/README.md +233 -0
- package/dist/config/index.d.ts +73 -0
- package/dist/config/index.js +125 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +162 -0
- package/dist/mcp-client/client.d.ts +36 -0
- package/dist/mcp-client/client.js +276 -0
- package/dist/mcp-client/configLoader.d.ts +75 -0
- package/dist/mcp-client/configLoader.js +203 -0
- package/dist/mcp-client/index.d.ts +10 -0
- package/dist/mcp-client/index.js +14 -0
- package/dist/mcp-client/transport.d.ts +34 -0
- package/dist/mcp-client/transport.js +183 -0
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +38 -0
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +40 -0
- package/dist/mcp-server/resources/echoResource/index.d.ts +5 -0
- package/dist/mcp-server/resources/echoResource/index.js +5 -0
- package/dist/mcp-server/resources/echoResource/registration.d.ts +12 -0
- package/dist/mcp-server/resources/echoResource/registration.js +122 -0
- package/dist/mcp-server/server.d.ts +27 -0
- package/dist/mcp-server/server.js +176 -0
- package/dist/mcp-server/tools/echoTool/echoToolLogic.d.ts +68 -0
- package/dist/mcp-server/tools/echoTool/echoToolLogic.js +73 -0
- package/dist/mcp-server/tools/echoTool/index.d.ts +5 -0
- package/dist/mcp-server/tools/echoTool/index.js +5 -0
- package/dist/mcp-server/tools/echoTool/registration.d.ts +12 -0
- package/dist/mcp-server/tools/echoTool/registration.js +86 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +57 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.js +145 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
- package/dist/mcp-server/transports/httpTransport.js +411 -0
- package/dist/mcp-server/transports/stdioTransport.d.ts +40 -0
- package/dist/mcp-server/transports/stdioTransport.js +70 -0
- package/dist/types-global/errors.d.ts +73 -0
- package/dist/types-global/errors.js +66 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/internal/errorHandler.d.ts +90 -0
- package/dist/utils/internal/errorHandler.js +247 -0
- package/dist/utils/internal/index.d.ts +3 -0
- package/dist/utils/internal/index.js +3 -0
- package/dist/utils/internal/logger.d.ts +50 -0
- package/dist/utils/internal/logger.js +267 -0
- package/dist/utils/internal/requestContext.d.ts +47 -0
- package/dist/utils/internal/requestContext.js +48 -0
- package/dist/utils/metrics/index.d.ts +1 -0
- package/dist/utils/metrics/index.js +1 -0
- package/dist/utils/metrics/tokenCounter.d.ts +27 -0
- package/dist/utils/metrics/tokenCounter.js +124 -0
- package/dist/utils/parsing/dateParser.d.ts +27 -0
- package/dist/utils/parsing/dateParser.js +62 -0
- package/dist/utils/parsing/index.d.ts +2 -0
- package/dist/utils/parsing/index.js +2 -0
- package/dist/utils/parsing/jsonParser.d.ts +46 -0
- package/dist/utils/parsing/jsonParser.js +79 -0
- package/dist/utils/security/idGenerator.d.ts +93 -0
- package/dist/utils/security/idGenerator.js +147 -0
- package/dist/utils/security/index.d.ts +3 -0
- package/dist/utils/security/index.js +3 -0
- package/dist/utils/security/rateLimiter.d.ts +92 -0
- package/dist/utils/security/rateLimiter.js +171 -0
- package/dist/utils/security/sanitization.d.ts +180 -0
- package/dist/utils/security/sanitization.js +372 -0
- package/package.json +79 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import sanitizeHtml from 'sanitize-html';
|
|
2
|
+
/**
|
|
3
|
+
* Options for path sanitization.
|
|
4
|
+
*/
|
|
5
|
+
export interface PathSanitizeOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Restrict paths to a specific root directory.
|
|
8
|
+
* If provided, the sanitized path will be relative to this root,
|
|
9
|
+
* and attempts to traverse above this root will be prevented.
|
|
10
|
+
*/
|
|
11
|
+
rootDir?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Normalize Windows-style backslashes (`\\`) to POSIX-style forward slashes (`/`).
|
|
14
|
+
* Defaults to `false`.
|
|
15
|
+
*/
|
|
16
|
+
toPosix?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Allow absolute paths.
|
|
19
|
+
* If `false` (default), absolute paths will be converted to relative paths
|
|
20
|
+
* (by removing leading slashes or drive letters).
|
|
21
|
+
* If `true`, absolute paths are permitted, subject to `rootDir` constraints if provided.
|
|
22
|
+
*/
|
|
23
|
+
allowAbsolute?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Information returned by the sanitizePath method, providing details about the sanitization process.
|
|
27
|
+
*/
|
|
28
|
+
export interface SanitizedPathInfo {
|
|
29
|
+
/** The final sanitized and normalized path string. */
|
|
30
|
+
sanitizedPath: string;
|
|
31
|
+
/** The original path string passed to the function before any normalization or sanitization. */
|
|
32
|
+
originalInput: string;
|
|
33
|
+
/** Indicates if the input path was determined to be absolute after initial `path.normalize()`. */
|
|
34
|
+
wasAbsolute: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Indicates if an initially absolute path was converted to a relative path.
|
|
37
|
+
* This typically happens if `options.allowAbsolute` was `false`.
|
|
38
|
+
*/
|
|
39
|
+
convertedToRelative: boolean;
|
|
40
|
+
/** The effective options that were used for sanitization, including defaults. */
|
|
41
|
+
optionsUsed: PathSanitizeOptions;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Context-specific input sanitization options
|
|
45
|
+
*/
|
|
46
|
+
export interface SanitizeStringOptions {
|
|
47
|
+
/** Handle content differently based on context */
|
|
48
|
+
context?: 'text' | 'html' | 'attribute' | 'url' | 'javascript';
|
|
49
|
+
/** Custom allowed tags when using html context */
|
|
50
|
+
allowedTags?: string[];
|
|
51
|
+
/** Custom allowed attributes when using html context */
|
|
52
|
+
allowedAttributes?: Record<string, string[]>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Configuration for HTML sanitization
|
|
56
|
+
*/
|
|
57
|
+
export interface HtmlSanitizeConfig {
|
|
58
|
+
/** Allowed HTML tags */
|
|
59
|
+
allowedTags?: string[];
|
|
60
|
+
/** Allowed HTML attributes (global or per-tag) */
|
|
61
|
+
allowedAttributes?: sanitizeHtml.IOptions['allowedAttributes'];
|
|
62
|
+
/** Allow preserving comments - uses allowedTags internally */
|
|
63
|
+
preserveComments?: boolean;
|
|
64
|
+
/** Custom URL sanitizer */
|
|
65
|
+
transformTags?: sanitizeHtml.IOptions['transformTags'];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Sanitization class for handling various input sanitization tasks.
|
|
69
|
+
* Provides methods to clean and validate strings, HTML, URLs, paths, JSON, and numbers.
|
|
70
|
+
*/
|
|
71
|
+
export declare class Sanitization {
|
|
72
|
+
private static instance;
|
|
73
|
+
/** Default list of sensitive fields for sanitizing logs */
|
|
74
|
+
private sensitiveFields;
|
|
75
|
+
/** Default sanitize-html configuration */
|
|
76
|
+
private defaultHtmlSanitizeConfig;
|
|
77
|
+
/**
|
|
78
|
+
* Private constructor to enforce singleton pattern.
|
|
79
|
+
*/
|
|
80
|
+
private constructor();
|
|
81
|
+
/**
|
|
82
|
+
* Get the singleton Sanitization instance.
|
|
83
|
+
* @returns {Sanitization} The singleton instance.
|
|
84
|
+
*/
|
|
85
|
+
static getInstance(): Sanitization;
|
|
86
|
+
/**
|
|
87
|
+
* Set sensitive fields for log sanitization. These fields will be redacted when
|
|
88
|
+
* `sanitizeForLogging` is called.
|
|
89
|
+
* @param {string[]} fields - Array of field names to consider sensitive.
|
|
90
|
+
*/
|
|
91
|
+
setSensitiveFields(fields: string[]): void;
|
|
92
|
+
/**
|
|
93
|
+
* Get the current list of sensitive fields used for log sanitization.
|
|
94
|
+
* @returns {string[]} Array of sensitive field names.
|
|
95
|
+
*/
|
|
96
|
+
getSensitiveFields(): string[];
|
|
97
|
+
/**
|
|
98
|
+
* Sanitize HTML content using the `sanitize-html` library.
|
|
99
|
+
* Removes potentially malicious tags and attributes.
|
|
100
|
+
* @param {string} input - HTML string to sanitize.
|
|
101
|
+
* @param {HtmlSanitizeConfig} [config] - Optional custom sanitization configuration.
|
|
102
|
+
* @returns {string} Sanitized HTML string.
|
|
103
|
+
*/
|
|
104
|
+
sanitizeHtml(input: string, config?: HtmlSanitizeConfig): string;
|
|
105
|
+
/**
|
|
106
|
+
* Sanitize string input based on context.
|
|
107
|
+
*
|
|
108
|
+
* **Important:** Using `context: 'javascript'` is explicitly disallowed and will throw an `McpError`.
|
|
109
|
+
* This is a security measure to prevent accidental execution or ineffective sanitization of JavaScript code.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} input - String to sanitize.
|
|
112
|
+
* @param {SanitizeStringOptions} [options={}] - Sanitization options.
|
|
113
|
+
* @returns {string} Sanitized string.
|
|
114
|
+
* @throws {McpError} If `context: 'javascript'` is used.
|
|
115
|
+
*/
|
|
116
|
+
sanitizeString(input: string, options?: SanitizeStringOptions): string;
|
|
117
|
+
/**
|
|
118
|
+
* Sanitize URL with robust validation.
|
|
119
|
+
* Ensures the URL uses allowed protocols and is well-formed.
|
|
120
|
+
* @param {string} input - URL to sanitize.
|
|
121
|
+
* @param {string[]} [allowedProtocols=['http', 'https']] - Allowed URL protocols.
|
|
122
|
+
* @returns {string} Sanitized URL.
|
|
123
|
+
* @throws {McpError} If URL is invalid or uses a disallowed protocol.
|
|
124
|
+
*/
|
|
125
|
+
sanitizeUrl(input: string, allowedProtocols?: string[]): string;
|
|
126
|
+
/**
|
|
127
|
+
* Sanitizes a file path to prevent path traversal and other common attacks.
|
|
128
|
+
* Normalizes the path, optionally converts to POSIX style, and can restrict
|
|
129
|
+
* the path to a root directory.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} input - The file path to sanitize.
|
|
132
|
+
* @param {PathSanitizeOptions} [options={}] - Options to control sanitization behavior.
|
|
133
|
+
* @returns {SanitizedPathInfo} An object containing the sanitized path and metadata about the sanitization process.
|
|
134
|
+
* @throws {McpError} If the path is invalid, unsafe (e.g., contains null bytes, attempts traversal).
|
|
135
|
+
*/
|
|
136
|
+
sanitizePath(input: string, options?: PathSanitizeOptions): SanitizedPathInfo;
|
|
137
|
+
/**
|
|
138
|
+
* Sanitize a JSON string. Validates format and optionally checks size.
|
|
139
|
+
* @template T - The expected type of the parsed JSON object.
|
|
140
|
+
* @param {string} input - JSON string to sanitize.
|
|
141
|
+
* @param {number} [maxSize] - Maximum allowed size in bytes.
|
|
142
|
+
* @returns {T} Parsed and sanitized object.
|
|
143
|
+
* @throws {McpError} If JSON is invalid, too large, or input is not a string.
|
|
144
|
+
*/
|
|
145
|
+
sanitizeJson<T = unknown>(input: string, maxSize?: number): T;
|
|
146
|
+
/**
|
|
147
|
+
* Ensure input is a valid number and optionally within a numeric range.
|
|
148
|
+
* Clamps the number to the range if min/max are provided and value is outside.
|
|
149
|
+
* @param {number | string} input - Number or string to validate.
|
|
150
|
+
* @param {number} [min] - Minimum allowed value (inclusive).
|
|
151
|
+
* @param {number} [max] - Maximum allowed value (inclusive).
|
|
152
|
+
* @returns {number} Sanitized number.
|
|
153
|
+
* @throws {McpError} If input is not a valid number or parsable string.
|
|
154
|
+
*/
|
|
155
|
+
sanitizeNumber(input: number | string, min?: number, max?: number): number;
|
|
156
|
+
/**
|
|
157
|
+
* Sanitize input for logging to protect sensitive information.
|
|
158
|
+
* Deep clones the input and redacts fields matching `this.sensitiveFields`.
|
|
159
|
+
* @param {unknown} input - Input to sanitize.
|
|
160
|
+
* @returns {unknown} Sanitized input safe for logging.
|
|
161
|
+
*/
|
|
162
|
+
sanitizeForLogging(input: unknown): unknown;
|
|
163
|
+
/**
|
|
164
|
+
* Private helper to convert attribute format for sanitize-html.
|
|
165
|
+
*/
|
|
166
|
+
private convertAttributesFormat;
|
|
167
|
+
/**
|
|
168
|
+
* Recursively redact sensitive fields in an object or array.
|
|
169
|
+
* Modifies the object in place.
|
|
170
|
+
* @param {unknown} obj - The object or array to redact.
|
|
171
|
+
*/
|
|
172
|
+
private redactSensitiveFields;
|
|
173
|
+
}
|
|
174
|
+
export declare const sanitization: Sanitization;
|
|
175
|
+
/**
|
|
176
|
+
* Convenience function to sanitize input for logging.
|
|
177
|
+
* @param {unknown} input - Input to sanitize.
|
|
178
|
+
* @returns {unknown} Sanitized input safe for logging.
|
|
179
|
+
*/
|
|
180
|
+
export declare const sanitizeInputForLogging: (input: unknown) => unknown;
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import sanitizeHtml from 'sanitize-html';
|
|
3
|
+
import validator from 'validator';
|
|
4
|
+
import { BaseErrorCode, McpError } from '../../types-global/errors.js';
|
|
5
|
+
// Import utils from the main barrel file (logger from ../internal/logger.js)
|
|
6
|
+
import { logger } from '../index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Sanitization class for handling various input sanitization tasks.
|
|
9
|
+
* Provides methods to clean and validate strings, HTML, URLs, paths, JSON, and numbers.
|
|
10
|
+
*/
|
|
11
|
+
export class Sanitization {
|
|
12
|
+
/**
|
|
13
|
+
* Private constructor to enforce singleton pattern.
|
|
14
|
+
*/
|
|
15
|
+
constructor() {
|
|
16
|
+
/** Default list of sensitive fields for sanitizing logs */
|
|
17
|
+
this.sensitiveFields = [
|
|
18
|
+
'password', 'token', 'secret', 'key', 'apiKey', 'auth',
|
|
19
|
+
'credential', 'jwt', 'ssn', 'credit', 'card', 'cvv', 'authorization'
|
|
20
|
+
];
|
|
21
|
+
/** Default sanitize-html configuration */
|
|
22
|
+
this.defaultHtmlSanitizeConfig = {
|
|
23
|
+
allowedTags: [
|
|
24
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'ul', 'ol',
|
|
25
|
+
'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br',
|
|
26
|
+
'div', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'pre'
|
|
27
|
+
],
|
|
28
|
+
allowedAttributes: {
|
|
29
|
+
'a': ['href', 'name', 'target'],
|
|
30
|
+
'img': ['src', 'alt', 'title', 'width', 'height'],
|
|
31
|
+
'*': ['class', 'id', 'style']
|
|
32
|
+
},
|
|
33
|
+
preserveComments: false
|
|
34
|
+
};
|
|
35
|
+
// Constructor intentionally left blank for singleton.
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the singleton Sanitization instance.
|
|
39
|
+
* @returns {Sanitization} The singleton instance.
|
|
40
|
+
*/
|
|
41
|
+
static getInstance() {
|
|
42
|
+
if (!Sanitization.instance) {
|
|
43
|
+
Sanitization.instance = new Sanitization();
|
|
44
|
+
}
|
|
45
|
+
return Sanitization.instance;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Set sensitive fields for log sanitization. These fields will be redacted when
|
|
49
|
+
* `sanitizeForLogging` is called.
|
|
50
|
+
* @param {string[]} fields - Array of field names to consider sensitive.
|
|
51
|
+
*/
|
|
52
|
+
setSensitiveFields(fields) {
|
|
53
|
+
this.sensitiveFields = [...new Set([...this.sensitiveFields, ...fields])]; // Ensure uniqueness
|
|
54
|
+
logger.debug('Updated sensitive fields list', { count: this.sensitiveFields.length });
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the current list of sensitive fields used for log sanitization.
|
|
58
|
+
* @returns {string[]} Array of sensitive field names.
|
|
59
|
+
*/
|
|
60
|
+
getSensitiveFields() {
|
|
61
|
+
return [...this.sensitiveFields];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Sanitize HTML content using the `sanitize-html` library.
|
|
65
|
+
* Removes potentially malicious tags and attributes.
|
|
66
|
+
* @param {string} input - HTML string to sanitize.
|
|
67
|
+
* @param {HtmlSanitizeConfig} [config] - Optional custom sanitization configuration.
|
|
68
|
+
* @returns {string} Sanitized HTML string.
|
|
69
|
+
*/
|
|
70
|
+
sanitizeHtml(input, config) {
|
|
71
|
+
if (!input)
|
|
72
|
+
return '';
|
|
73
|
+
const effectiveConfig = { ...this.defaultHtmlSanitizeConfig, ...config };
|
|
74
|
+
const options = {
|
|
75
|
+
allowedTags: effectiveConfig.allowedTags,
|
|
76
|
+
allowedAttributes: effectiveConfig.allowedAttributes,
|
|
77
|
+
transformTags: effectiveConfig.transformTags
|
|
78
|
+
};
|
|
79
|
+
if (effectiveConfig.preserveComments) {
|
|
80
|
+
options.allowedTags = [...(options.allowedTags || []), '!--'];
|
|
81
|
+
}
|
|
82
|
+
return sanitizeHtml(input, options);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Sanitize string input based on context.
|
|
86
|
+
*
|
|
87
|
+
* **Important:** Using `context: 'javascript'` is explicitly disallowed and will throw an `McpError`.
|
|
88
|
+
* This is a security measure to prevent accidental execution or ineffective sanitization of JavaScript code.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} input - String to sanitize.
|
|
91
|
+
* @param {SanitizeStringOptions} [options={}] - Sanitization options.
|
|
92
|
+
* @returns {string} Sanitized string.
|
|
93
|
+
* @throws {McpError} If `context: 'javascript'` is used.
|
|
94
|
+
*/
|
|
95
|
+
sanitizeString(input, options = {}) {
|
|
96
|
+
if (!input)
|
|
97
|
+
return '';
|
|
98
|
+
switch (options.context) {
|
|
99
|
+
case 'html':
|
|
100
|
+
return this.sanitizeHtml(input, {
|
|
101
|
+
allowedTags: options.allowedTags,
|
|
102
|
+
allowedAttributes: options.allowedAttributes ?
|
|
103
|
+
this.convertAttributesFormat(options.allowedAttributes) :
|
|
104
|
+
undefined
|
|
105
|
+
});
|
|
106
|
+
case 'attribute':
|
|
107
|
+
return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
|
|
108
|
+
case 'url':
|
|
109
|
+
if (!validator.isURL(input, { protocols: ['http', 'https'], require_protocol: true })) {
|
|
110
|
+
logger.warning('Invalid URL detected during string sanitization', { input });
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
return validator.trim(input);
|
|
114
|
+
case 'javascript':
|
|
115
|
+
logger.error('Attempted JavaScript sanitization via sanitizeString', { input: input.substring(0, 50) });
|
|
116
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'JavaScript sanitization not supported through string sanitizer');
|
|
117
|
+
case 'text':
|
|
118
|
+
default:
|
|
119
|
+
return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Sanitize URL with robust validation.
|
|
124
|
+
* Ensures the URL uses allowed protocols and is well-formed.
|
|
125
|
+
* @param {string} input - URL to sanitize.
|
|
126
|
+
* @param {string[]} [allowedProtocols=['http', 'https']] - Allowed URL protocols.
|
|
127
|
+
* @returns {string} Sanitized URL.
|
|
128
|
+
* @throws {McpError} If URL is invalid or uses a disallowed protocol.
|
|
129
|
+
*/
|
|
130
|
+
sanitizeUrl(input, allowedProtocols = ['http', 'https']) {
|
|
131
|
+
try {
|
|
132
|
+
if (!validator.isURL(input, { protocols: allowedProtocols, require_protocol: true })) {
|
|
133
|
+
throw new Error('Invalid URL format or protocol');
|
|
134
|
+
}
|
|
135
|
+
const lowerInput = input.toLowerCase().trim();
|
|
136
|
+
if (lowerInput.startsWith('javascript:')) { // Double-check against javascript:
|
|
137
|
+
throw new Error('JavaScript protocol not allowed');
|
|
138
|
+
}
|
|
139
|
+
return validator.trim(input);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, error instanceof Error ? error.message : 'Invalid URL format', { input });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Sanitizes a file path to prevent path traversal and other common attacks.
|
|
147
|
+
* Normalizes the path, optionally converts to POSIX style, and can restrict
|
|
148
|
+
* the path to a root directory.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} input - The file path to sanitize.
|
|
151
|
+
* @param {PathSanitizeOptions} [options={}] - Options to control sanitization behavior.
|
|
152
|
+
* @returns {SanitizedPathInfo} An object containing the sanitized path and metadata about the sanitization process.
|
|
153
|
+
* @throws {McpError} If the path is invalid, unsafe (e.g., contains null bytes, attempts traversal).
|
|
154
|
+
*/
|
|
155
|
+
sanitizePath(input, options = {}) {
|
|
156
|
+
const originalInput = input;
|
|
157
|
+
const effectiveOptions = {
|
|
158
|
+
toPosix: options.toPosix ?? false,
|
|
159
|
+
allowAbsolute: options.allowAbsolute ?? false,
|
|
160
|
+
rootDir: options.rootDir
|
|
161
|
+
};
|
|
162
|
+
let wasAbsoluteInitially = false;
|
|
163
|
+
let convertedToRelative = false;
|
|
164
|
+
try {
|
|
165
|
+
if (!input || typeof input !== 'string') {
|
|
166
|
+
throw new Error('Invalid path input: must be a non-empty string');
|
|
167
|
+
}
|
|
168
|
+
let normalized = path.normalize(input);
|
|
169
|
+
wasAbsoluteInitially = path.isAbsolute(normalized);
|
|
170
|
+
if (normalized.includes('\0')) {
|
|
171
|
+
throw new Error('Path contains null byte');
|
|
172
|
+
}
|
|
173
|
+
if (effectiveOptions.toPosix) {
|
|
174
|
+
normalized = normalized.replace(/\\/g, '/');
|
|
175
|
+
}
|
|
176
|
+
if (!effectiveOptions.allowAbsolute && path.isAbsolute(normalized)) {
|
|
177
|
+
// Original path was absolute, but absolute paths are not allowed.
|
|
178
|
+
// Convert to relative by stripping leading slash or drive letter.
|
|
179
|
+
normalized = normalized.replace(/^(?:[A-Za-z]:)?[/\\]+/, '');
|
|
180
|
+
convertedToRelative = true;
|
|
181
|
+
}
|
|
182
|
+
let finalSanitizedPath;
|
|
183
|
+
if (effectiveOptions.rootDir) {
|
|
184
|
+
const rootDirResolved = path.resolve(effectiveOptions.rootDir);
|
|
185
|
+
// If 'normalized' is absolute (and allowed), path.resolve uses it as the base.
|
|
186
|
+
// If 'normalized' is relative, it's resolved against 'rootDirResolved'.
|
|
187
|
+
const fullPath = path.resolve(rootDirResolved, normalized);
|
|
188
|
+
if (!fullPath.startsWith(rootDirResolved + path.sep) && fullPath !== rootDirResolved) {
|
|
189
|
+
throw new Error('Path traversal detected (escapes rootDir)');
|
|
190
|
+
}
|
|
191
|
+
// Path is within rootDir, return it relative to rootDir.
|
|
192
|
+
finalSanitizedPath = path.relative(rootDirResolved, fullPath);
|
|
193
|
+
// Ensure empty string result from path.relative (if fullPath equals rootDirResolved) becomes '.'
|
|
194
|
+
finalSanitizedPath = finalSanitizedPath === '' ? '.' : finalSanitizedPath;
|
|
195
|
+
}
|
|
196
|
+
else { // No rootDir specified
|
|
197
|
+
if (path.isAbsolute(normalized)) {
|
|
198
|
+
if (effectiveOptions.allowAbsolute) {
|
|
199
|
+
// Absolute path is allowed and no rootDir to constrain it.
|
|
200
|
+
finalSanitizedPath = normalized;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Should not happen if logic above is correct (already made relative or was originally relative)
|
|
204
|
+
// but as a safeguard:
|
|
205
|
+
throw new Error('Absolute path encountered when not allowed and not rooted');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else { // Path is relative and no rootDir
|
|
209
|
+
if (normalized.includes('..')) {
|
|
210
|
+
const resolvedPath = path.resolve(normalized); // Resolves relative to CWD
|
|
211
|
+
const currentWorkingDir = path.resolve('.');
|
|
212
|
+
if (!resolvedPath.startsWith(currentWorkingDir)) {
|
|
213
|
+
throw new Error('Relative path traversal detected (escapes CWD)');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
finalSanitizedPath = normalized;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
sanitizedPath: finalSanitizedPath,
|
|
221
|
+
originalInput,
|
|
222
|
+
wasAbsolute: wasAbsoluteInitially,
|
|
223
|
+
convertedToRelative,
|
|
224
|
+
optionsUsed: effectiveOptions
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
logger.warning('Path sanitization error', {
|
|
229
|
+
input: originalInput,
|
|
230
|
+
options: effectiveOptions,
|
|
231
|
+
error: error instanceof Error ? error.message : String(error)
|
|
232
|
+
});
|
|
233
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, error instanceof Error ? error.message : 'Invalid or unsafe path', { input: originalInput } // Provide original input in error details
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Sanitize a JSON string. Validates format and optionally checks size.
|
|
239
|
+
* @template T - The expected type of the parsed JSON object.
|
|
240
|
+
* @param {string} input - JSON string to sanitize.
|
|
241
|
+
* @param {number} [maxSize] - Maximum allowed size in bytes.
|
|
242
|
+
* @returns {T} Parsed and sanitized object.
|
|
243
|
+
* @throws {McpError} If JSON is invalid, too large, or input is not a string.
|
|
244
|
+
*/
|
|
245
|
+
sanitizeJson(input, maxSize) {
|
|
246
|
+
try {
|
|
247
|
+
if (typeof input !== 'string') {
|
|
248
|
+
throw new Error('Invalid input: expected a JSON string');
|
|
249
|
+
}
|
|
250
|
+
if (maxSize !== undefined && Buffer.byteLength(input, 'utf8') > maxSize) {
|
|
251
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `JSON exceeds maximum allowed size of ${maxSize} bytes`, { size: Buffer.byteLength(input, 'utf8'), maxSize });
|
|
252
|
+
}
|
|
253
|
+
const parsed = JSON.parse(input);
|
|
254
|
+
// Optional: Add recursive sanitization of parsed object values if needed
|
|
255
|
+
// this.sanitizeObjectRecursively(parsed);
|
|
256
|
+
return parsed;
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (error instanceof McpError)
|
|
260
|
+
throw error;
|
|
261
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, error instanceof Error ? error.message : 'Invalid JSON format', { input: input.length > 100 ? `${input.substring(0, 100)}...` : input });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Ensure input is a valid number and optionally within a numeric range.
|
|
266
|
+
* Clamps the number to the range if min/max are provided and value is outside.
|
|
267
|
+
* @param {number | string} input - Number or string to validate.
|
|
268
|
+
* @param {number} [min] - Minimum allowed value (inclusive).
|
|
269
|
+
* @param {number} [max] - Maximum allowed value (inclusive).
|
|
270
|
+
* @returns {number} Sanitized number.
|
|
271
|
+
* @throws {McpError} If input is not a valid number or parsable string.
|
|
272
|
+
*/
|
|
273
|
+
sanitizeNumber(input, min, max) {
|
|
274
|
+
let value;
|
|
275
|
+
if (typeof input === 'string') {
|
|
276
|
+
if (!validator.isNumeric(input.trim())) {
|
|
277
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Invalid number format', { input });
|
|
278
|
+
}
|
|
279
|
+
value = parseFloat(input.trim());
|
|
280
|
+
}
|
|
281
|
+
else if (typeof input === 'number') {
|
|
282
|
+
value = input;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Invalid input type: expected number or string', { input: String(input) });
|
|
286
|
+
}
|
|
287
|
+
if (isNaN(value) || !isFinite(value)) {
|
|
288
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Invalid number value (NaN or Infinity)', { input });
|
|
289
|
+
}
|
|
290
|
+
let clamped = false;
|
|
291
|
+
if (min !== undefined && value < min) {
|
|
292
|
+
value = min;
|
|
293
|
+
clamped = true;
|
|
294
|
+
}
|
|
295
|
+
if (max !== undefined && value > max) {
|
|
296
|
+
value = max;
|
|
297
|
+
clamped = true;
|
|
298
|
+
}
|
|
299
|
+
if (clamped) {
|
|
300
|
+
logger.debug('Number clamped to range', { input, min, max, finalValue: value });
|
|
301
|
+
}
|
|
302
|
+
return value;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Sanitize input for logging to protect sensitive information.
|
|
306
|
+
* Deep clones the input and redacts fields matching `this.sensitiveFields`.
|
|
307
|
+
* @param {unknown} input - Input to sanitize.
|
|
308
|
+
* @returns {unknown} Sanitized input safe for logging.
|
|
309
|
+
*/
|
|
310
|
+
sanitizeForLogging(input) {
|
|
311
|
+
try {
|
|
312
|
+
if (!input || typeof input !== 'object') {
|
|
313
|
+
return input;
|
|
314
|
+
}
|
|
315
|
+
const clonedInput = typeof structuredClone === 'function'
|
|
316
|
+
? structuredClone(input)
|
|
317
|
+
: JSON.parse(JSON.stringify(input)); // Fallback for older Node versions
|
|
318
|
+
this.redactSensitiveFields(clonedInput);
|
|
319
|
+
return clonedInput;
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
logger.error('Error during log sanitization', {
|
|
323
|
+
error: error instanceof Error ? error.message : String(error)
|
|
324
|
+
});
|
|
325
|
+
return '[Log Sanitization Failed]';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Private helper to convert attribute format for sanitize-html.
|
|
330
|
+
*/
|
|
331
|
+
convertAttributesFormat(attrs) {
|
|
332
|
+
return attrs;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Recursively redact sensitive fields in an object or array.
|
|
336
|
+
* Modifies the object in place.
|
|
337
|
+
* @param {unknown} obj - The object or array to redact.
|
|
338
|
+
*/
|
|
339
|
+
redactSensitiveFields(obj) {
|
|
340
|
+
if (!obj || typeof obj !== 'object') {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (Array.isArray(obj)) {
|
|
344
|
+
obj.forEach(item => {
|
|
345
|
+
if (item && typeof item === 'object') {
|
|
346
|
+
this.redactSensitiveFields(item);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
for (const key in obj) {
|
|
352
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
353
|
+
const value = obj[key];
|
|
354
|
+
const isSensitive = this.sensitiveFields.some(field => key.toLowerCase().includes(field.toLowerCase()));
|
|
355
|
+
if (isSensitive) {
|
|
356
|
+
obj[key] = '[REDACTED]';
|
|
357
|
+
}
|
|
358
|
+
else if (value && typeof value === 'object') {
|
|
359
|
+
this.redactSensitiveFields(value);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Create and export singleton instance
|
|
366
|
+
export const sanitization = Sanitization.getInstance();
|
|
367
|
+
/**
|
|
368
|
+
* Convenience function to sanitize input for logging.
|
|
369
|
+
* @param {unknown} input - Input to sanitize.
|
|
370
|
+
* @returns {unknown} Sanitized input safe for logging.
|
|
371
|
+
*/
|
|
372
|
+
export const sanitizeInputForLogging = (input) => sanitization.sanitizeForLogging(input);
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-ts-template",
|
|
3
|
+
"version": "1.1.6",
|
|
4
|
+
"description": "TypeScript template for building Model Context Protocol (MCP) servers & clients. Features production-ready utilities, stdio/HTTP transports (with JWT auth), examples, and type safety. Ideal starting point for creating MCP-based applications.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"bin": {
|
|
10
|
+
"mcp-ts-template": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/cyanheads/mcp-ts-template.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/cyanheads/mcp-ts-template/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/cyanheads/mcp-ts-template#readme",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc && node --loader ts-node/esm scripts/make-executable.ts dist/index.js",
|
|
23
|
+
"start": "node dist/index.js",
|
|
24
|
+
"start:stdio": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=stdio node dist/index.js",
|
|
25
|
+
"start:http": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=http node dist/index.js",
|
|
26
|
+
"rebuild": "ts-node --esm scripts/clean.ts && npm run build",
|
|
27
|
+
"docs:generate": "typedoc",
|
|
28
|
+
"tree": "ts-node --esm scripts/tree.ts",
|
|
29
|
+
"fetch-spec": "ts-node --esm scripts/fetch-openapi-spec.ts",
|
|
30
|
+
"inspector": "mcp-inspector --config mcp.json --server mcp-ts-template"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
34
|
+
"@types/jsonwebtoken": "^9.0.9",
|
|
35
|
+
"@types/node": "^22.15.15",
|
|
36
|
+
"@types/sanitize-html": "^2.16.0",
|
|
37
|
+
"@types/validator": "13.15.0",
|
|
38
|
+
"chrono-node": "^2.8.0",
|
|
39
|
+
"dotenv": "^16.5.0",
|
|
40
|
+
"express": "^5.1.0",
|
|
41
|
+
"ignore": "^7.0.4",
|
|
42
|
+
"jsonwebtoken": "^9.0.2",
|
|
43
|
+
"openai": "^4.97.0",
|
|
44
|
+
"partial-json": "^0.1.7",
|
|
45
|
+
"sanitize-html": "^2.16.0",
|
|
46
|
+
"tiktoken": "^1.0.21",
|
|
47
|
+
"ts-node": "^10.9.2",
|
|
48
|
+
"typescript": "^5.8.3",
|
|
49
|
+
"validator": "13.15.0",
|
|
50
|
+
"winston": "^3.17.0",
|
|
51
|
+
"winston-daily-rotate-file": "^5.0.0",
|
|
52
|
+
"yargs": "^17.7.2",
|
|
53
|
+
"zod": "^3.24.4"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"typescript",
|
|
57
|
+
"template",
|
|
58
|
+
"MCP",
|
|
59
|
+
"model-context-protocol",
|
|
60
|
+
"LLM",
|
|
61
|
+
"AI-integration",
|
|
62
|
+
"server",
|
|
63
|
+
"client",
|
|
64
|
+
"sdk",
|
|
65
|
+
"http",
|
|
66
|
+
"sse",
|
|
67
|
+
"jwt",
|
|
68
|
+
"authentication"
|
|
69
|
+
],
|
|
70
|
+
"author": "Casey Hand @cyanheads",
|
|
71
|
+
"license": "Apache-2.0",
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/express": "^5.0.1",
|
|
74
|
+
"@types/js-yaml": "^4.0.9",
|
|
75
|
+
"axios": "^1.9.0",
|
|
76
|
+
"js-yaml": "^4.1.0",
|
|
77
|
+
"typedoc": "^0.28.4"
|
|
78
|
+
}
|
|
79
|
+
}
|