@berthojoris/mcp-mysql-server 1.4.3 → 1.4.5

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.
@@ -1,4 +1,4 @@
1
- import { FeatureConfig } from '../config/featureConfig.js';
1
+ import { FeatureConfig } from "../config/featureConfig.js";
2
2
  export declare class SecurityLayer {
3
3
  private ajv;
4
4
  private readonly dangerousKeywords;
@@ -22,8 +22,10 @@ export declare class SecurityLayer {
22
22
  };
23
23
  /**
24
24
  * Validate SQL query for security issues
25
+ * @param query - The SQL query to validate
26
+ * @param bypassDangerousCheck - If true, skips dangerous keyword check (for users with 'execute' permission)
25
27
  */
26
- validateQuery(query: string): {
28
+ validateQuery(query: string, bypassDangerousCheck?: boolean): {
27
29
  valid: boolean;
28
30
  error?: string;
29
31
  queryType?: string;
@@ -44,6 +46,10 @@ export declare class SecurityLayer {
44
46
  * Check if a query contains dangerous operations
45
47
  */
46
48
  hasDangerousOperations(query: string): boolean;
49
+ /**
50
+ * Check if execute permission is enabled
51
+ */
52
+ hasExecutePermission(): boolean;
47
53
  /**
48
54
  * Escape identifier for safe use in SQL queries
49
55
  */
@@ -10,18 +10,24 @@ class SecurityLayer {
10
10
  constructor(featureConfig) {
11
11
  this.ajv = new ajv_1.default();
12
12
  this.featureConfig = featureConfig || new featureConfig_js_1.FeatureConfig();
13
- // Define dangerous SQL keywords that should always be blocked (security threats)
13
+ // Define dangerous SQL keywords that should ALWAYS be blocked (critical security threats)
14
+ // These are blocked even with 'execute' permission
14
15
  this.dangerousKeywords = [
15
- 'GRANT', 'REVOKE', 'LOAD_FILE', 'INTO OUTFILE', 'INTO DUMPFILE',
16
- 'LOAD DATA', 'INFORMATION_SCHEMA', 'MYSQL', 'PERFORMANCE_SCHEMA',
17
- 'SYS', 'SHOW', 'DESCRIBE', 'DESC', 'EXPLAIN', 'PROCEDURE',
18
- 'FUNCTION', 'TRIGGER', 'EVENT', 'VIEW', 'INDEX', 'DATABASE',
19
- 'SCHEMA', 'USER', 'PASSWORD', 'SLEEP', 'BENCHMARK'
16
+ "GRANT",
17
+ "REVOKE",
18
+ "INTO OUTFILE",
19
+ "INTO DUMPFILE",
20
+ "LOAD DATA",
21
+ "MYSQL",
22
+ "PERFORMANCE_SCHEMA",
23
+ "SYS",
24
+ "USER",
25
+ "PASSWORD",
20
26
  ];
21
27
  // Define basic allowed SQL operations
22
- this.allowedOperations = ['SELECT', 'INSERT', 'UPDATE', 'DELETE'];
28
+ this.allowedOperations = ["SELECT", "INSERT", "UPDATE", "DELETE"];
23
29
  // Define DDL operations that require special permission
24
- this.ddlOperations = ['CREATE', 'ALTER', 'DROP', 'TRUNCATE', 'RENAME'];
30
+ this.ddlOperations = ["CREATE", "ALTER", "DROP", "TRUNCATE", "RENAME"];
25
31
  }
26
32
  /**
27
33
  * Validate input against a JSON schema
@@ -32,7 +38,7 @@ class SecurityLayer {
32
38
  if (!valid) {
33
39
  return {
34
40
  valid: false,
35
- errors: validate.errors
41
+ errors: validate.errors,
36
42
  };
37
43
  }
38
44
  return { valid: true };
@@ -41,52 +47,88 @@ class SecurityLayer {
41
47
  * Validate and sanitize table/column names to prevent SQL injection
42
48
  */
43
49
  validateIdentifier(identifier) {
44
- if (!identifier || typeof identifier !== 'string') {
45
- return { valid: false, error: 'Identifier must be a non-empty string' };
50
+ if (!identifier || typeof identifier !== "string") {
51
+ return { valid: false, error: "Identifier must be a non-empty string" };
46
52
  }
47
53
  // Check length
48
54
  if (identifier.length > 64) {
49
- return { valid: false, error: 'Identifier too long (max 64 characters)' };
55
+ return { valid: false, error: "Identifier too long (max 64 characters)" };
50
56
  }
51
57
  // MySQL identifier rules: alphanumeric, underscore, dollar sign
52
58
  // Must start with letter, underscore, or dollar sign
53
59
  const identifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
54
60
  if (!identifierRegex.test(identifier)) {
55
- return { valid: false, error: 'Invalid identifier format' };
61
+ return { valid: false, error: "Invalid identifier format" };
56
62
  }
57
63
  // Check against MySQL reserved words (basic list)
58
64
  const reservedWords = [
59
- 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'FROM', 'WHERE', 'JOIN',
60
- 'INNER', 'LEFT', 'RIGHT', 'OUTER', 'ON', 'AS', 'AND', 'OR', 'NOT',
61
- 'NULL', 'TRUE', 'FALSE', 'ORDER', 'BY', 'GROUP', 'HAVING', 'LIMIT',
62
- 'OFFSET', 'DISTINCT', 'ALL', 'EXISTS', 'IN', 'BETWEEN', 'LIKE',
63
- 'REGEXP', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'IF', 'IFNULL'
65
+ "SELECT",
66
+ "INSERT",
67
+ "UPDATE",
68
+ "DELETE",
69
+ "FROM",
70
+ "WHERE",
71
+ "JOIN",
72
+ "INNER",
73
+ "LEFT",
74
+ "RIGHT",
75
+ "OUTER",
76
+ "ON",
77
+ "AS",
78
+ "AND",
79
+ "OR",
80
+ "NOT",
81
+ "NULL",
82
+ "TRUE",
83
+ "FALSE",
84
+ "ORDER",
85
+ "BY",
86
+ "GROUP",
87
+ "HAVING",
88
+ "LIMIT",
89
+ "OFFSET",
90
+ "DISTINCT",
91
+ "ALL",
92
+ "EXISTS",
93
+ "IN",
94
+ "BETWEEN",
95
+ "LIKE",
96
+ "REGEXP",
97
+ "CASE",
98
+ "WHEN",
99
+ "THEN",
100
+ "ELSE",
101
+ "END",
102
+ "IF",
103
+ "IFNULL",
64
104
  ];
65
105
  if (reservedWords.includes(identifier.toUpperCase())) {
66
- return { valid: false, error: 'Identifier cannot be a reserved word' };
106
+ return { valid: false, error: "Identifier cannot be a reserved word" };
67
107
  }
68
108
  return { valid: true };
69
109
  }
70
110
  /**
71
111
  * Validate SQL query for security issues
112
+ * @param query - The SQL query to validate
113
+ * @param bypassDangerousCheck - If true, skips dangerous keyword check (for users with 'execute' permission)
72
114
  */
73
- validateQuery(query) {
74
- if (!query || typeof query !== 'string') {
75
- return { valid: false, error: 'Query must be a non-empty string' };
115
+ validateQuery(query, bypassDangerousCheck = false) {
116
+ if (!query || typeof query !== "string") {
117
+ return { valid: false, error: "Query must be a non-empty string" };
76
118
  }
77
119
  const trimmedQuery = query.trim().toUpperCase();
78
120
  // Check for empty query
79
121
  if (trimmedQuery.length === 0) {
80
- return { valid: false, error: 'Query cannot be empty' };
122
+ return { valid: false, error: "Query cannot be empty" };
81
123
  }
82
124
  // Check for multiple statements (basic check)
83
- if (query.includes(';') && !query.trim().endsWith(';')) {
84
- return { valid: false, error: 'Multiple statements not allowed' };
125
+ if (query.includes(";") && !query.trim().endsWith(";")) {
126
+ return { valid: false, error: "Multiple statements not allowed" };
85
127
  }
86
128
  // Remove trailing semicolon for analysis
87
- const cleanQuery = trimmedQuery.replace(/;$/, '');
129
+ const cleanQuery = trimmedQuery.replace(/;$/, "");
88
130
  // Determine query type - check basic operations first
89
- let queryType = '';
131
+ let queryType = "";
90
132
  for (const operation of this.allowedOperations) {
91
133
  if (cleanQuery.startsWith(operation)) {
92
134
  queryType = operation;
@@ -105,35 +147,50 @@ class SecurityLayer {
105
147
  else {
106
148
  return {
107
149
  valid: false,
108
- error: `DDL operation '${ddlOp}' requires 'ddl' permission. Add 'ddl' to your permissions configuration.`
150
+ error: `DDL operation '${ddlOp}' requires 'ddl' permission. Add 'ddl' to your permissions configuration.`,
109
151
  };
110
152
  }
111
153
  }
112
154
  }
113
155
  }
114
156
  if (!queryType) {
115
- return { valid: false, error: 'Query type not allowed' };
157
+ return { valid: false, error: "Query type not allowed" };
116
158
  }
117
- // Check for dangerous keywords (always blocked regardless of permissions)
118
- for (const keyword of this.dangerousKeywords) {
119
- if (cleanQuery.includes(keyword)) {
120
- return { valid: false, error: `Dangerous keyword detected: ${keyword}` };
159
+ // Check for dangerous keywords (blocked unless user has 'execute' permission)
160
+ // When bypassDangerousCheck is true (user has 'execute' permission), skip this check
161
+ if (!bypassDangerousCheck) {
162
+ for (const keyword of this.dangerousKeywords) {
163
+ if (cleanQuery.includes(keyword)) {
164
+ return {
165
+ valid: false,
166
+ error: `Dangerous keyword detected: ${keyword}. This requires 'execute' permission.`,
167
+ };
168
+ }
121
169
  }
122
170
  }
123
171
  // Additional checks for specific query types
124
- if (queryType === 'SELECT') {
172
+ // Only enforce these restrictions when user doesn't have 'execute' permission
173
+ if (queryType === "SELECT" && !bypassDangerousCheck) {
125
174
  // Check for UNION attacks
126
- if (cleanQuery.includes('UNION')) {
127
- return { valid: false, error: 'UNION operations not allowed' };
175
+ if (cleanQuery.includes("UNION")) {
176
+ return {
177
+ valid: false,
178
+ error: "UNION operations not allowed without 'execute' permission",
179
+ };
128
180
  }
129
181
  // Check for subqueries in FROM clause (basic check)
130
- if (cleanQuery.includes('FROM (')) {
131
- return { valid: false, error: 'Subqueries in FROM clause not allowed' };
182
+ if (cleanQuery.includes("FROM (")) {
183
+ return {
184
+ valid: false,
185
+ error: "Subqueries in FROM clause not allowed without 'execute' permission",
186
+ };
132
187
  }
133
188
  }
134
189
  // Check for comment-based injection attempts
135
- if (cleanQuery.includes('/*') || cleanQuery.includes('--') || cleanQuery.includes('#')) {
136
- return { valid: false, error: 'Comments not allowed in queries' };
190
+ if (cleanQuery.includes("/*") ||
191
+ cleanQuery.includes("--") ||
192
+ cleanQuery.includes("#")) {
193
+ return { valid: false, error: "Comments not allowed in queries" };
137
194
  }
138
195
  return { valid: true, queryType };
139
196
  }
@@ -145,7 +202,7 @@ class SecurityLayer {
145
202
  return { valid: true, sanitizedParams: [] };
146
203
  }
147
204
  if (!Array.isArray(params)) {
148
- return { valid: false, error: 'Parameters must be an array' };
205
+ return { valid: false, error: "Parameters must be an array" };
149
206
  }
150
207
  const sanitizedParams = [];
151
208
  for (let i = 0; i < params.length; i++) {
@@ -156,29 +213,38 @@ class SecurityLayer {
156
213
  continue;
157
214
  }
158
215
  // Validate based on type
159
- if (typeof param === 'string') {
216
+ if (typeof param === "string") {
160
217
  // Check string length
161
218
  if (param.length > 65535) {
162
- return { valid: false, error: `Parameter ${i} too long (max 65535 characters)` };
219
+ return {
220
+ valid: false,
221
+ error: `Parameter ${i} too long (max 65535 characters)`,
222
+ };
163
223
  }
164
224
  // Don't modify strings - let MySQL handle escaping through prepared statements
165
225
  sanitizedParams.push(param);
166
226
  }
167
- else if (typeof param === 'number') {
227
+ else if (typeof param === "number") {
168
228
  // Validate number
169
229
  if (!Number.isFinite(param)) {
170
- return { valid: false, error: `Parameter ${i} must be a finite number` };
230
+ return {
231
+ valid: false,
232
+ error: `Parameter ${i} must be a finite number`,
233
+ };
171
234
  }
172
235
  sanitizedParams.push(param);
173
236
  }
174
- else if (typeof param === 'boolean') {
237
+ else if (typeof param === "boolean") {
175
238
  sanitizedParams.push(param);
176
239
  }
177
240
  else if (param instanceof Date) {
178
241
  sanitizedParams.push(param);
179
242
  }
180
243
  else {
181
- return { valid: false, error: `Parameter ${i} has unsupported type: ${typeof param}` };
244
+ return {
245
+ valid: false,
246
+ error: `Parameter ${i} has unsupported type: ${typeof param}`,
247
+ };
182
248
  }
183
249
  }
184
250
  return { valid: true, sanitizedParams };
@@ -188,7 +254,7 @@ class SecurityLayer {
188
254
  */
189
255
  isReadOnlyQuery(query) {
190
256
  const validation = this.validateQuery(query);
191
- return validation.valid && validation.queryType === 'SELECT';
257
+ return validation.valid && validation.queryType === "SELECT";
192
258
  }
193
259
  /**
194
260
  * Check if a query contains dangerous operations
@@ -197,6 +263,12 @@ class SecurityLayer {
197
263
  const validation = this.validateQuery(query);
198
264
  return !validation.valid;
199
265
  }
266
+ /**
267
+ * Check if execute permission is enabled
268
+ */
269
+ hasExecutePermission() {
270
+ return this.featureConfig.isCategoryEnabled(featureConfig_js_1.ToolCategory.EXECUTE);
271
+ }
200
272
  /**
201
273
  * Escape identifier for safe use in SQL queries
202
274
  */
@@ -1,4 +1,4 @@
1
- import SecurityLayer from '../security/securityLayer';
1
+ import SecurityLayer from "../security/securityLayer";
2
2
  export declare class QueryTools {
3
3
  private db;
4
4
  private security;
@@ -18,46 +18,49 @@ class QueryTools {
18
18
  // Validate input schema
19
19
  if (!(0, schemas_1.validateRunQuery)(queryParams)) {
20
20
  return {
21
- status: 'error',
22
- error: 'Invalid parameters: ' + JSON.stringify(schemas_1.validateRunQuery.errors)
21
+ status: "error",
22
+ error: "Invalid parameters: " + JSON.stringify(schemas_1.validateRunQuery.errors),
23
23
  };
24
24
  }
25
25
  try {
26
26
  const { query, params = [] } = queryParams;
27
+ // Check if user has execute permission to bypass dangerous keyword checks
28
+ const hasExecutePermission = this.security.hasExecutePermission();
27
29
  // Validate query using security layer
28
- const queryValidation = this.security.validateQuery(query);
30
+ // If user has execute permission, allow advanced SQL features
31
+ const queryValidation = this.security.validateQuery(query, hasExecutePermission);
29
32
  if (!queryValidation.valid) {
30
33
  return {
31
- status: 'error',
32
- error: `Query validation failed: ${queryValidation.error}`
34
+ status: "error",
35
+ error: `Query validation failed: ${queryValidation.error}`,
33
36
  };
34
37
  }
35
38
  // Ensure it's a SELECT query
36
- if (queryValidation.queryType !== 'SELECT') {
39
+ if (queryValidation.queryType !== "SELECT") {
37
40
  return {
38
- status: 'error',
39
- error: 'Only SELECT queries are allowed with runQuery. Use executeSql for other operations.'
41
+ status: "error",
42
+ error: "Only SELECT queries are allowed with runQuery. Use executeSql for other operations.",
40
43
  };
41
44
  }
42
45
  // Validate parameters
43
46
  const paramValidation = this.security.validateParameters(params);
44
47
  if (!paramValidation.valid) {
45
48
  return {
46
- status: 'error',
47
- error: `Parameter validation failed: ${paramValidation.error}`
49
+ status: "error",
50
+ error: `Parameter validation failed: ${paramValidation.error}`,
48
51
  };
49
52
  }
50
53
  // Execute the query with sanitized parameters
51
54
  const results = await this.db.query(query, paramValidation.sanitizedParams);
52
55
  return {
53
- status: 'success',
54
- data: results
56
+ status: "success",
57
+ data: results,
55
58
  };
56
59
  }
57
60
  catch (error) {
58
61
  return {
59
- status: 'error',
60
- error: error.message
62
+ status: "error",
63
+ error: error.message,
61
64
  };
62
65
  }
63
66
  }
@@ -69,49 +72,50 @@ class QueryTools {
69
72
  // Validate input schema
70
73
  if (!(0, schemas_1.validateRunQuery)(queryParams)) {
71
74
  return {
72
- status: 'error',
73
- error: 'Invalid parameters: ' + JSON.stringify(schemas_1.validateRunQuery.errors)
75
+ status: "error",
76
+ error: "Invalid parameters: " + JSON.stringify(schemas_1.validateRunQuery.errors),
74
77
  };
75
78
  }
76
79
  try {
77
80
  const { query, params = [] } = queryParams;
78
81
  // Validate query using security layer
79
- const queryValidation = this.security.validateQuery(query);
82
+ // Pass true for bypassDangerousCheck since this is executeSql (requires 'execute' permission)
83
+ const queryValidation = this.security.validateQuery(query, true);
80
84
  if (!queryValidation.valid) {
81
85
  return {
82
- status: 'error',
83
- error: `Query validation failed: ${queryValidation.error}`
86
+ status: "error",
87
+ error: `Query validation failed: ${queryValidation.error}`,
84
88
  };
85
89
  }
86
90
  // Ensure it's not a SELECT query (use runQuery for that)
87
- if (queryValidation.queryType === 'SELECT') {
91
+ if (queryValidation.queryType === "SELECT") {
88
92
  return {
89
- status: 'error',
90
- error: 'SELECT queries should use runQuery method instead of executeSql.'
93
+ status: "error",
94
+ error: "SELECT queries should use runQuery method instead of executeSql.",
91
95
  };
92
96
  }
93
97
  // Validate parameters
94
98
  const paramValidation = this.security.validateParameters(params);
95
99
  if (!paramValidation.valid) {
96
100
  return {
97
- status: 'error',
98
- error: `Parameter validation failed: ${paramValidation.error}`
101
+ status: "error",
102
+ error: `Parameter validation failed: ${paramValidation.error}`,
99
103
  };
100
104
  }
101
105
  // Execute the query with sanitized parameters
102
106
  const result = await this.db.query(query, paramValidation.sanitizedParams);
103
107
  return {
104
- status: 'success',
108
+ status: "success",
105
109
  data: {
106
110
  affectedRows: result.affectedRows || 0,
107
- insertId: result.insertId || null
108
- }
111
+ insertId: result.insertId || null,
112
+ },
109
113
  };
110
114
  }
111
115
  catch (error) {
112
116
  return {
113
- status: 'error',
114
- error: error.message
117
+ status: "error",
118
+ error: error.message,
115
119
  };
116
120
  }
117
121
  }
package/package.json CHANGED
@@ -1,86 +1,86 @@
1
- {
2
- "name": "@berthojoris/mcp-mysql-server",
3
- "version": "1.4.3",
4
- "description": "Model Context Protocol server for MySQL database integration with dynamic per-project permissions and data export capabilities",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "bin": {
8
- "mcp-mysql": "./bin/mcp-mysql.js"
9
- },
10
- "scripts": {
11
- "build": "tsc",
12
- "start": "node dist/server.js",
13
- "start:mcp": "node dist/mcp-server.js",
14
- "start:api": "node dist/server.js",
15
- "dev": "ts-node src/index.ts",
16
- "dev:mcp": "ts-node src/mcp-server.ts",
17
- "dev:api": "ts-node src/server.ts",
18
- "test": "jest",
19
- "prepare": "npm run build",
20
- "prepublishOnly": "npm run build"
21
- },
22
- "keywords": [
23
- "mcp",
24
- "mysql",
25
- "database",
26
- "llm",
27
- "ai",
28
- "model-context-protocol",
29
- "claude",
30
- "cline",
31
- "windsurf",
32
- "agent",
33
- "database-tools",
34
- "sql",
35
- "mysql-client",
36
- "ai-tools"
37
- ],
38
- "author": "Bertho Joris <berthojoris@gmail.com>",
39
- "license": "MIT",
40
- "repository": {
41
- "type": "git",
42
- "url": "https://github.com/berthojoris/mysql-mcp.git"
43
- },
44
- "bugs": {
45
- "url": "https://github.com/berthojoris/mysql-mcp/issues"
46
- },
47
- "homepage": "https://github.com/berthojoris/mysql-mcp#readme",
48
- "files": [
49
- "dist",
50
- "bin",
51
- "README.md",
52
- "CHANGELOG.md",
53
- "LICENSE",
54
- "manifest.json"
55
- ],
56
- "engines": {
57
- "node": ">=18.0.0"
58
- },
59
- "dependencies": {
60
- "@modelcontextprotocol/sdk": "^1.20.0",
61
- "ajv": "^8.12.0",
62
- "cors": "^2.8.5",
63
- "dotenv": "^16.3.1",
64
- "express": "^4.18.2",
65
- "express-rate-limit": "^7.1.5",
66
- "helmet": "^7.1.0",
67
- "jsonwebtoken": "^9.0.2",
68
- "morgan": "^1.10.0",
69
- "mysql2": "^3.6.1",
70
- "winston": "^3.11.0"
71
- },
72
- "devDependencies": {
73
- "@types/cors": "^2.8.17",
74
- "@types/express": "^4.17.21",
75
- "@types/helmet": "^0.0.48",
76
- "@types/jest": "^29.5.4",
77
- "@types/jsonwebtoken": "^9.0.5",
78
- "@types/morgan": "^1.9.9",
79
- "@types/node": "^20.6.0",
80
- "@types/winston": "^2.4.4",
81
- "jest": "^29.6.4",
82
- "ts-jest": "^29.1.1",
83
- "ts-node": "^10.9.1",
84
- "typescript": "^5.2.2"
85
- }
86
- }
1
+ {
2
+ "name": "@berthojoris/mcp-mysql-server",
3
+ "version": "1.4.5",
4
+ "description": "Model Context Protocol server for MySQL database integration with dynamic per-project permissions and data export capabilities",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "mcp-mysql": "./bin/mcp-mysql.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/server.js",
13
+ "start:mcp": "node dist/mcp-server.js",
14
+ "start:api": "node dist/server.js",
15
+ "dev": "ts-node src/index.ts",
16
+ "dev:mcp": "ts-node src/mcp-server.ts",
17
+ "dev:api": "ts-node src/server.ts",
18
+ "test": "jest",
19
+ "prepare": "npm run build",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "mysql",
25
+ "database",
26
+ "llm",
27
+ "ai",
28
+ "model-context-protocol",
29
+ "claude",
30
+ "cline",
31
+ "windsurf",
32
+ "agent",
33
+ "database-tools",
34
+ "sql",
35
+ "mysql-client",
36
+ "ai-tools"
37
+ ],
38
+ "author": "Bertho Joris <berthojoris@gmail.com>",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/berthojoris/mysql-mcp.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/berthojoris/mysql-mcp/issues"
46
+ },
47
+ "homepage": "https://github.com/berthojoris/mysql-mcp#readme",
48
+ "files": [
49
+ "dist",
50
+ "bin",
51
+ "README.md",
52
+ "CHANGELOG.md",
53
+ "LICENSE",
54
+ "manifest.json"
55
+ ],
56
+ "engines": {
57
+ "node": ">=18.0.0"
58
+ },
59
+ "dependencies": {
60
+ "@modelcontextprotocol/sdk": "^1.20.0",
61
+ "ajv": "^8.12.0",
62
+ "cors": "^2.8.5",
63
+ "dotenv": "^16.3.1",
64
+ "express": "^4.18.2",
65
+ "express-rate-limit": "^7.1.5",
66
+ "helmet": "^7.1.0",
67
+ "jsonwebtoken": "^9.0.2",
68
+ "morgan": "^1.10.0",
69
+ "mysql2": "^3.6.1",
70
+ "winston": "^3.11.0"
71
+ },
72
+ "devDependencies": {
73
+ "@types/cors": "^2.8.17",
74
+ "@types/express": "^4.17.21",
75
+ "@types/helmet": "^0.0.48",
76
+ "@types/jest": "^29.5.4",
77
+ "@types/jsonwebtoken": "^9.0.5",
78
+ "@types/morgan": "^1.9.9",
79
+ "@types/node": "^20.6.0",
80
+ "@types/winston": "^2.4.4",
81
+ "jest": "^29.6.4",
82
+ "ts-jest": "^29.1.1",
83
+ "ts-node": "^10.9.1",
84
+ "typescript": "^5.2.2"
85
+ }
86
+ }