@berthojoris/mcp-mysql-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1142 -0
- package/bin/mcp-mysql.js +122 -0
- package/dist/auth/authService.d.ts +29 -0
- package/dist/auth/authService.js +114 -0
- package/dist/config/config.d.ts +12 -0
- package/dist/config/config.js +19 -0
- package/dist/config/featureConfig.d.ts +51 -0
- package/dist/config/featureConfig.js +130 -0
- package/dist/db/connection.d.ts +22 -0
- package/dist/db/connection.js +135 -0
- package/dist/index.d.ts +236 -0
- package/dist/index.js +273 -0
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +748 -0
- package/dist/security/securityLayer.d.ts +52 -0
- package/dist/security/securityLayer.js +213 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +283 -0
- package/dist/tools/crudTools.d.ts +59 -0
- package/dist/tools/crudTools.js +443 -0
- package/dist/tools/databaseTools.d.ts +33 -0
- package/dist/tools/databaseTools.js +108 -0
- package/dist/tools/ddlTools.d.ts +69 -0
- package/dist/tools/ddlTools.js +199 -0
- package/dist/tools/queryTools.d.ts +29 -0
- package/dist/tools/queryTools.js +119 -0
- package/dist/tools/storedProcedureTools.d.ts +80 -0
- package/dist/tools/storedProcedureTools.js +411 -0
- package/dist/tools/transactionTools.d.ts +45 -0
- package/dist/tools/transactionTools.js +130 -0
- package/dist/tools/utilityTools.d.ts +30 -0
- package/dist/tools/utilityTools.js +121 -0
- package/dist/validation/schemas.d.ts +423 -0
- package/dist/validation/schemas.js +287 -0
- package/manifest.json +248 -0
- package/package.json +83 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { FeatureConfig } from '../config/featureConfig.js';
|
|
2
|
+
export declare class SecurityLayer {
|
|
3
|
+
private ajv;
|
|
4
|
+
private readonly dangerousKeywords;
|
|
5
|
+
private readonly allowedOperations;
|
|
6
|
+
private readonly ddlOperations;
|
|
7
|
+
private featureConfig;
|
|
8
|
+
constructor(featureConfig?: FeatureConfig);
|
|
9
|
+
/**
|
|
10
|
+
* Validate input against a JSON schema
|
|
11
|
+
*/
|
|
12
|
+
validateInput(schema: object, data: any): {
|
|
13
|
+
valid: boolean;
|
|
14
|
+
errors?: any;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Validate and sanitize table/column names to prevent SQL injection
|
|
18
|
+
*/
|
|
19
|
+
validateIdentifier(identifier: string): {
|
|
20
|
+
valid: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Validate SQL query for security issues
|
|
25
|
+
*/
|
|
26
|
+
validateQuery(query: string): {
|
|
27
|
+
valid: boolean;
|
|
28
|
+
error?: string;
|
|
29
|
+
queryType?: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Validate parameter values to prevent injection
|
|
33
|
+
*/
|
|
34
|
+
validateParameters(params: any[]): {
|
|
35
|
+
valid: boolean;
|
|
36
|
+
error?: string;
|
|
37
|
+
sanitizedParams?: any[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Check if a query is a read-only SELECT query
|
|
41
|
+
*/
|
|
42
|
+
isReadOnlyQuery(query: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Check if a query contains dangerous operations
|
|
45
|
+
*/
|
|
46
|
+
hasDangerousOperations(query: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Escape identifier for safe use in SQL queries
|
|
49
|
+
*/
|
|
50
|
+
escapeIdentifier(identifier: string): string;
|
|
51
|
+
}
|
|
52
|
+
export default SecurityLayer;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SecurityLayer = void 0;
|
|
7
|
+
const ajv_1 = __importDefault(require("ajv"));
|
|
8
|
+
const featureConfig_js_1 = require("../config/featureConfig.js");
|
|
9
|
+
class SecurityLayer {
|
|
10
|
+
constructor(featureConfig) {
|
|
11
|
+
this.ajv = new ajv_1.default();
|
|
12
|
+
this.featureConfig = featureConfig || new featureConfig_js_1.FeatureConfig();
|
|
13
|
+
// Define dangerous SQL keywords that should always be blocked (security threats)
|
|
14
|
+
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'
|
|
20
|
+
];
|
|
21
|
+
// Define basic allowed SQL operations
|
|
22
|
+
this.allowedOperations = ['SELECT', 'INSERT', 'UPDATE', 'DELETE'];
|
|
23
|
+
// Define DDL operations that require special permission
|
|
24
|
+
this.ddlOperations = ['CREATE', 'ALTER', 'DROP', 'TRUNCATE', 'RENAME'];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate input against a JSON schema
|
|
28
|
+
*/
|
|
29
|
+
validateInput(schema, data) {
|
|
30
|
+
const validate = this.ajv.compile(schema);
|
|
31
|
+
const valid = validate(data);
|
|
32
|
+
if (!valid) {
|
|
33
|
+
return {
|
|
34
|
+
valid: false,
|
|
35
|
+
errors: validate.errors
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { valid: true };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validate and sanitize table/column names to prevent SQL injection
|
|
42
|
+
*/
|
|
43
|
+
validateIdentifier(identifier) {
|
|
44
|
+
if (!identifier || typeof identifier !== 'string') {
|
|
45
|
+
return { valid: false, error: 'Identifier must be a non-empty string' };
|
|
46
|
+
}
|
|
47
|
+
// Check length
|
|
48
|
+
if (identifier.length > 64) {
|
|
49
|
+
return { valid: false, error: 'Identifier too long (max 64 characters)' };
|
|
50
|
+
}
|
|
51
|
+
// MySQL identifier rules: alphanumeric, underscore, dollar sign
|
|
52
|
+
// Must start with letter, underscore, or dollar sign
|
|
53
|
+
const identifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
54
|
+
if (!identifierRegex.test(identifier)) {
|
|
55
|
+
return { valid: false, error: 'Invalid identifier format' };
|
|
56
|
+
}
|
|
57
|
+
// Check against MySQL reserved words (basic list)
|
|
58
|
+
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'
|
|
64
|
+
];
|
|
65
|
+
if (reservedWords.includes(identifier.toUpperCase())) {
|
|
66
|
+
return { valid: false, error: 'Identifier cannot be a reserved word' };
|
|
67
|
+
}
|
|
68
|
+
return { valid: true };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Validate SQL query for security issues
|
|
72
|
+
*/
|
|
73
|
+
validateQuery(query) {
|
|
74
|
+
if (!query || typeof query !== 'string') {
|
|
75
|
+
return { valid: false, error: 'Query must be a non-empty string' };
|
|
76
|
+
}
|
|
77
|
+
const trimmedQuery = query.trim().toUpperCase();
|
|
78
|
+
// Check for empty query
|
|
79
|
+
if (trimmedQuery.length === 0) {
|
|
80
|
+
return { valid: false, error: 'Query cannot be empty' };
|
|
81
|
+
}
|
|
82
|
+
// Check for multiple statements (basic check)
|
|
83
|
+
if (query.includes(';') && !query.trim().endsWith(';')) {
|
|
84
|
+
return { valid: false, error: 'Multiple statements not allowed' };
|
|
85
|
+
}
|
|
86
|
+
// Remove trailing semicolon for analysis
|
|
87
|
+
const cleanQuery = trimmedQuery.replace(/;$/, '');
|
|
88
|
+
// Determine query type - check basic operations first
|
|
89
|
+
let queryType = '';
|
|
90
|
+
for (const operation of this.allowedOperations) {
|
|
91
|
+
if (cleanQuery.startsWith(operation)) {
|
|
92
|
+
queryType = operation;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// If not a basic operation, check if it's a DDL operation
|
|
97
|
+
if (!queryType) {
|
|
98
|
+
for (const ddlOp of this.ddlOperations) {
|
|
99
|
+
if (cleanQuery.startsWith(ddlOp)) {
|
|
100
|
+
// Check if DDL permission is enabled
|
|
101
|
+
if (this.featureConfig.isCategoryEnabled(featureConfig_js_1.ToolCategory.DDL)) {
|
|
102
|
+
queryType = ddlOp;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
return {
|
|
107
|
+
valid: false,
|
|
108
|
+
error: `DDL operation '${ddlOp}' requires 'ddl' permission. Add 'ddl' to your permissions configuration.`
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!queryType) {
|
|
115
|
+
return { valid: false, error: 'Query type not allowed' };
|
|
116
|
+
}
|
|
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}` };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Additional checks for specific query types
|
|
124
|
+
if (queryType === 'SELECT') {
|
|
125
|
+
// Check for UNION attacks
|
|
126
|
+
if (cleanQuery.includes('UNION')) {
|
|
127
|
+
return { valid: false, error: 'UNION operations not allowed' };
|
|
128
|
+
}
|
|
129
|
+
// Check for subqueries in FROM clause (basic check)
|
|
130
|
+
if (cleanQuery.includes('FROM (')) {
|
|
131
|
+
return { valid: false, error: 'Subqueries in FROM clause not allowed' };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Check for comment-based injection attempts
|
|
135
|
+
if (cleanQuery.includes('/*') || cleanQuery.includes('--') || cleanQuery.includes('#')) {
|
|
136
|
+
return { valid: false, error: 'Comments not allowed in queries' };
|
|
137
|
+
}
|
|
138
|
+
return { valid: true, queryType };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Validate parameter values to prevent injection
|
|
142
|
+
*/
|
|
143
|
+
validateParameters(params) {
|
|
144
|
+
if (!params) {
|
|
145
|
+
return { valid: true, sanitizedParams: [] };
|
|
146
|
+
}
|
|
147
|
+
if (!Array.isArray(params)) {
|
|
148
|
+
return { valid: false, error: 'Parameters must be an array' };
|
|
149
|
+
}
|
|
150
|
+
const sanitizedParams = [];
|
|
151
|
+
for (let i = 0; i < params.length; i++) {
|
|
152
|
+
const param = params[i];
|
|
153
|
+
// Check for null/undefined
|
|
154
|
+
if (param === null || param === undefined) {
|
|
155
|
+
sanitizedParams.push(null);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
// Validate based on type
|
|
159
|
+
if (typeof param === 'string') {
|
|
160
|
+
// Check string length
|
|
161
|
+
if (param.length > 65535) {
|
|
162
|
+
return { valid: false, error: `Parameter ${i} too long (max 65535 characters)` };
|
|
163
|
+
}
|
|
164
|
+
// Don't modify strings - let MySQL handle escaping through prepared statements
|
|
165
|
+
sanitizedParams.push(param);
|
|
166
|
+
}
|
|
167
|
+
else if (typeof param === 'number') {
|
|
168
|
+
// Validate number
|
|
169
|
+
if (!Number.isFinite(param)) {
|
|
170
|
+
return { valid: false, error: `Parameter ${i} must be a finite number` };
|
|
171
|
+
}
|
|
172
|
+
sanitizedParams.push(param);
|
|
173
|
+
}
|
|
174
|
+
else if (typeof param === 'boolean') {
|
|
175
|
+
sanitizedParams.push(param);
|
|
176
|
+
}
|
|
177
|
+
else if (param instanceof Date) {
|
|
178
|
+
sanitizedParams.push(param);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
return { valid: false, error: `Parameter ${i} has unsupported type: ${typeof param}` };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { valid: true, sanitizedParams };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Check if a query is a read-only SELECT query
|
|
188
|
+
*/
|
|
189
|
+
isReadOnlyQuery(query) {
|
|
190
|
+
const validation = this.validateQuery(query);
|
|
191
|
+
return validation.valid && validation.queryType === 'SELECT';
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Check if a query contains dangerous operations
|
|
195
|
+
*/
|
|
196
|
+
hasDangerousOperations(query) {
|
|
197
|
+
const validation = this.validateQuery(query);
|
|
198
|
+
return !validation.valid;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Escape identifier for safe use in SQL queries
|
|
202
|
+
*/
|
|
203
|
+
escapeIdentifier(identifier) {
|
|
204
|
+
const validation = this.validateIdentifier(identifier);
|
|
205
|
+
if (!validation.valid) {
|
|
206
|
+
throw new Error(`Invalid identifier: ${validation.error}`);
|
|
207
|
+
}
|
|
208
|
+
// Use backticks to escape MySQL identifiers
|
|
209
|
+
return `\`${identifier}\``;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
exports.SecurityLayer = SecurityLayer;
|
|
213
|
+
exports.default = SecurityLayer;
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const cors_1 = __importDefault(require("cors"));
|
|
8
|
+
const helmet_1 = __importDefault(require("helmet"));
|
|
9
|
+
const morgan_1 = __importDefault(require("morgan"));
|
|
10
|
+
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
|
11
|
+
const index_1 = require("./index");
|
|
12
|
+
const winston_1 = require("winston");
|
|
13
|
+
// Initialize the MCP instance
|
|
14
|
+
const mcp = new index_1.MySQLMCP();
|
|
15
|
+
// Create Winston logger
|
|
16
|
+
const logger = (0, winston_1.createLogger)({
|
|
17
|
+
level: 'info',
|
|
18
|
+
format: winston_1.format.combine(winston_1.format.timestamp(), winston_1.format.json()),
|
|
19
|
+
transports: [
|
|
20
|
+
new winston_1.transports.Console(),
|
|
21
|
+
new winston_1.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
|
22
|
+
new winston_1.transports.File({ filename: 'logs/combined.log' })
|
|
23
|
+
]
|
|
24
|
+
});
|
|
25
|
+
// Initialize Express app
|
|
26
|
+
const app = (0, express_1.default)();
|
|
27
|
+
const PORT = process.env.PORT || 3000;
|
|
28
|
+
// Middleware
|
|
29
|
+
app.use((0, helmet_1.default)()); // Security headers
|
|
30
|
+
app.use((0, cors_1.default)()); // Enable CORS
|
|
31
|
+
app.use(express_1.default.json()); // Parse JSON bodies
|
|
32
|
+
app.use((0, morgan_1.default)('combined')); // HTTP request logging
|
|
33
|
+
// Rate limiting
|
|
34
|
+
const apiLimiter = (0, express_rate_limit_1.default)({
|
|
35
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
36
|
+
max: 100, // Limit each IP to 100 requests per windowMs
|
|
37
|
+
standardHeaders: true,
|
|
38
|
+
legacyHeaders: false,
|
|
39
|
+
});
|
|
40
|
+
app.use(apiLimiter);
|
|
41
|
+
// No authentication middleware needed for MCP server
|
|
42
|
+
// Error handling middleware
|
|
43
|
+
const errorHandler = (err, req, res, next) => {
|
|
44
|
+
logger.error(`${err.name}: ${err.message}`, {
|
|
45
|
+
path: req.path,
|
|
46
|
+
method: req.method,
|
|
47
|
+
body: req.body,
|
|
48
|
+
stack: err.stack
|
|
49
|
+
});
|
|
50
|
+
res.status(500).json({
|
|
51
|
+
error: {
|
|
52
|
+
code: 'SERVER_ERROR',
|
|
53
|
+
message: 'An unexpected error occurred',
|
|
54
|
+
details: process.env.NODE_ENV === 'production' ? 'See server logs for details' : err.message
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
// Health check endpoint (no auth required)
|
|
59
|
+
app.get('/health', (req, res) => {
|
|
60
|
+
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
61
|
+
});
|
|
62
|
+
// Feature configuration status endpoint
|
|
63
|
+
app.get('/features', (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
const featureStatus = mcp.getFeatureStatus();
|
|
66
|
+
res.status(200).json(featureStatus);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
logger.error('Error getting feature status', { error });
|
|
70
|
+
res.status(500).json({
|
|
71
|
+
status: 'error',
|
|
72
|
+
error: 'Failed to retrieve feature configuration status'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// API routes - no authentication required for MCP server
|
|
77
|
+
const apiRouter = express_1.default.Router();
|
|
78
|
+
app.use('/api', apiRouter);
|
|
79
|
+
// Database Tools Routes
|
|
80
|
+
apiRouter.get('/databases', async (req, res, next) => {
|
|
81
|
+
try {
|
|
82
|
+
const result = await mcp.listDatabases();
|
|
83
|
+
res.json(result);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
next(error);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
apiRouter.get('/tables', async (req, res, next) => {
|
|
90
|
+
try {
|
|
91
|
+
const result = await mcp.listTables({ database: undefined });
|
|
92
|
+
res.json(result);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
next(error);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
apiRouter.get('/tables/:tableName/schema', async (req, res, next) => {
|
|
99
|
+
try {
|
|
100
|
+
const { tableName } = req.params;
|
|
101
|
+
const result = await mcp.readTableSchema({ table_name: tableName });
|
|
102
|
+
res.json(result);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
next(error);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
// CRUD Operations Routes
|
|
109
|
+
apiRouter.post('/tables/:tableName/records', async (req, res, next) => {
|
|
110
|
+
try {
|
|
111
|
+
const { tableName } = req.params;
|
|
112
|
+
const { data } = req.body;
|
|
113
|
+
if (!data) {
|
|
114
|
+
return res.status(400).json({
|
|
115
|
+
error: {
|
|
116
|
+
code: 'INVALID_INPUT',
|
|
117
|
+
message: 'Missing data field in request body',
|
|
118
|
+
details: 'The request body must contain a data object with the record fields'
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const result = await mcp.createRecord({
|
|
123
|
+
table_name: tableName,
|
|
124
|
+
data
|
|
125
|
+
});
|
|
126
|
+
res.status(201).json(result);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
next(error);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
apiRouter.get('/tables/:tableName/records', async (req, res, next) => {
|
|
133
|
+
try {
|
|
134
|
+
const { tableName } = req.params;
|
|
135
|
+
const { filters, limit, offset, sort_by, sort_direction } = req.query;
|
|
136
|
+
const result = await mcp.readRecords({
|
|
137
|
+
table_name: tableName,
|
|
138
|
+
filters: filters ? JSON.parse(filters) : undefined,
|
|
139
|
+
pagination: {
|
|
140
|
+
page: offset ? Math.floor(parseInt(offset) / (limit ? parseInt(limit) : 10)) + 1 : 1,
|
|
141
|
+
limit: limit ? parseInt(limit) : 10
|
|
142
|
+
},
|
|
143
|
+
sorting: sort_by ? {
|
|
144
|
+
field: sort_by,
|
|
145
|
+
direction: sort_direction?.toLowerCase() === 'desc' ? 'desc' : 'asc'
|
|
146
|
+
} : undefined
|
|
147
|
+
});
|
|
148
|
+
res.json(result);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
next(error);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
apiRouter.put('/tables/:tableName/records/:id', async (req, res, next) => {
|
|
155
|
+
try {
|
|
156
|
+
const { tableName, id } = req.params;
|
|
157
|
+
const { data, id_field } = req.body;
|
|
158
|
+
if (!data) {
|
|
159
|
+
return res.status(400).json({
|
|
160
|
+
error: {
|
|
161
|
+
code: 'INVALID_INPUT',
|
|
162
|
+
message: 'Missing data field in request body',
|
|
163
|
+
details: 'The request body must contain a data object with the fields to update'
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const result = await mcp.updateRecord({
|
|
168
|
+
table_name: tableName,
|
|
169
|
+
data,
|
|
170
|
+
conditions: [{ field: id_field || 'id', operator: '=', value: id }]
|
|
171
|
+
});
|
|
172
|
+
res.json(result);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
next(error);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
apiRouter.delete('/tables/:tableName/records/:id', async (req, res, next) => {
|
|
179
|
+
try {
|
|
180
|
+
const { tableName, id } = req.params;
|
|
181
|
+
const { id_field } = req.query;
|
|
182
|
+
const result = await mcp.deleteRecord({
|
|
183
|
+
table_name: tableName,
|
|
184
|
+
conditions: [{ field: id_field || 'id', operator: '=', value: id }]
|
|
185
|
+
});
|
|
186
|
+
res.json(result);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
next(error);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// Query Tools Routes
|
|
193
|
+
apiRouter.post('/query', async (req, res, next) => {
|
|
194
|
+
try {
|
|
195
|
+
const { query, params } = req.body;
|
|
196
|
+
if (!query) {
|
|
197
|
+
return res.status(400).json({
|
|
198
|
+
error: {
|
|
199
|
+
code: 'INVALID_INPUT',
|
|
200
|
+
message: 'Missing query field in request body',
|
|
201
|
+
details: 'The request body must contain a query string'
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const result = await mcp.runQuery({
|
|
206
|
+
query,
|
|
207
|
+
params: params || []
|
|
208
|
+
});
|
|
209
|
+
res.json(result);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
next(error);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
apiRouter.post('/execute', async (req, res, next) => {
|
|
216
|
+
try {
|
|
217
|
+
const { query, params } = req.body;
|
|
218
|
+
if (!query) {
|
|
219
|
+
return res.status(400).json({
|
|
220
|
+
error: {
|
|
221
|
+
code: 'INVALID_INPUT',
|
|
222
|
+
message: 'Missing query field in request body',
|
|
223
|
+
details: 'The request body must contain a query string'
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const result = await mcp.executeSql({
|
|
228
|
+
query,
|
|
229
|
+
params: params || []
|
|
230
|
+
});
|
|
231
|
+
res.json(result);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
next(error);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
// Utility Tools Routes
|
|
238
|
+
apiRouter.get('/connection', async (req, res, next) => {
|
|
239
|
+
try {
|
|
240
|
+
const result = await mcp.describeConnection();
|
|
241
|
+
res.json(result);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
next(error);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
apiRouter.get('/connection/test', async (req, res, next) => {
|
|
248
|
+
try {
|
|
249
|
+
const result = await mcp.testConnection();
|
|
250
|
+
res.json(result);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
next(error);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
apiRouter.get('/tables/:tableName/relationships', async (req, res, next) => {
|
|
257
|
+
try {
|
|
258
|
+
const { tableName } = req.params;
|
|
259
|
+
const result = await mcp.getTableRelationships({ table_name: tableName });
|
|
260
|
+
res.json(result);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
next(error);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
// Apply error handler
|
|
267
|
+
app.use(errorHandler);
|
|
268
|
+
// Start the server
|
|
269
|
+
const server = app.listen(PORT, () => {
|
|
270
|
+
logger.info(`MCP MySQL Server running on port ${PORT}`);
|
|
271
|
+
console.log(`MCP MySQL Server running on port ${PORT}`);
|
|
272
|
+
});
|
|
273
|
+
// Graceful shutdown
|
|
274
|
+
process.on('SIGTERM', () => {
|
|
275
|
+
logger.info('SIGTERM signal received: closing HTTP server');
|
|
276
|
+
server.close(async () => {
|
|
277
|
+
logger.info('HTTP server closed');
|
|
278
|
+
await mcp.close();
|
|
279
|
+
logger.info('Database connections closed');
|
|
280
|
+
process.exit(0);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
exports.default = app;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import SecurityLayer from '../security/securityLayer';
|
|
2
|
+
import { FilterCondition, Pagination, Sorting } from '../validation/schemas';
|
|
3
|
+
export declare class CrudTools {
|
|
4
|
+
private db;
|
|
5
|
+
private security;
|
|
6
|
+
constructor(security: SecurityLayer);
|
|
7
|
+
/**
|
|
8
|
+
* Create a new record in the specified table
|
|
9
|
+
*/
|
|
10
|
+
createRecord(params: {
|
|
11
|
+
table_name: string;
|
|
12
|
+
data: Record<string, any>;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
status: string;
|
|
15
|
+
data?: any;
|
|
16
|
+
error?: string;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* Read records from the specified table with optional filters, pagination, and sorting
|
|
20
|
+
*/
|
|
21
|
+
readRecords(params: {
|
|
22
|
+
table_name: string;
|
|
23
|
+
filters?: FilterCondition[];
|
|
24
|
+
pagination?: Pagination;
|
|
25
|
+
sorting?: Sorting;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
status: string;
|
|
28
|
+
data?: any[];
|
|
29
|
+
total?: number;
|
|
30
|
+
error?: string;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Update records in the specified table based on conditions
|
|
34
|
+
*/
|
|
35
|
+
updateRecord(params: {
|
|
36
|
+
table_name: string;
|
|
37
|
+
data: Record<string, any>;
|
|
38
|
+
conditions: FilterCondition[];
|
|
39
|
+
}): Promise<{
|
|
40
|
+
status: string;
|
|
41
|
+
data?: {
|
|
42
|
+
affectedRows: number;
|
|
43
|
+
};
|
|
44
|
+
error?: string;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Delete records from the specified table based on conditions
|
|
48
|
+
*/
|
|
49
|
+
deleteRecord(params: {
|
|
50
|
+
table_name: string;
|
|
51
|
+
conditions: FilterCondition[];
|
|
52
|
+
}): Promise<{
|
|
53
|
+
status: string;
|
|
54
|
+
data?: {
|
|
55
|
+
affectedRows: number;
|
|
56
|
+
};
|
|
57
|
+
error?: string;
|
|
58
|
+
}>;
|
|
59
|
+
}
|