@sigma4life/mysql-mcp-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.
Files changed (149) hide show
  1. package/.env.example +35 -0
  2. package/.github/workflows/ci.yml +18 -0
  3. package/AUTHENTICATION.md +24 -0
  4. package/CLAUDE.md +37 -0
  5. package/CONTRIBUTING.md +19 -0
  6. package/LICENSE +21 -0
  7. package/QUERY_ACCESS_SETUP.md +65 -0
  8. package/README.md +144 -0
  9. package/SECURITY.md +10 -0
  10. package/TESTING.md +54 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +197 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/core/cache.d.ts +11 -0
  16. package/dist/core/cache.d.ts.map +1 -0
  17. package/dist/core/cache.js +30 -0
  18. package/dist/core/cache.js.map +1 -0
  19. package/dist/core/config.d.ts +24 -0
  20. package/dist/core/config.d.ts.map +1 -0
  21. package/dist/core/config.js +63 -0
  22. package/dist/core/config.js.map +1 -0
  23. package/dist/core/database-url.d.ts +11 -0
  24. package/dist/core/database-url.d.ts.map +1 -0
  25. package/dist/core/database-url.js +37 -0
  26. package/dist/core/database-url.js.map +1 -0
  27. package/dist/core/database-url.test.d.ts +2 -0
  28. package/dist/core/database-url.test.d.ts.map +1 -0
  29. package/dist/core/database-url.test.js +22 -0
  30. package/dist/core/database-url.test.js.map +1 -0
  31. package/dist/core/logger.d.ts +3 -0
  32. package/dist/core/logger.d.ts.map +1 -0
  33. package/dist/core/logger.js +17 -0
  34. package/dist/core/logger.js.map +1 -0
  35. package/dist/core/mcp.d.ts +14 -0
  36. package/dist/core/mcp.d.ts.map +1 -0
  37. package/dist/core/mcp.js +23 -0
  38. package/dist/core/mcp.js.map +1 -0
  39. package/dist/core/query-safety.d.ts +10 -0
  40. package/dist/core/query-safety.d.ts.map +1 -0
  41. package/dist/core/query-safety.js +50 -0
  42. package/dist/core/query-safety.js.map +1 -0
  43. package/dist/core/query-safety.test.d.ts +2 -0
  44. package/dist/core/query-safety.test.d.ts.map +1 -0
  45. package/dist/core/query-safety.test.js +30 -0
  46. package/dist/core/query-safety.test.js.map +1 -0
  47. package/dist/core/schema-types.d.ts +53 -0
  48. package/dist/core/schema-types.d.ts.map +1 -0
  49. package/dist/core/schema-types.js +2 -0
  50. package/dist/core/schema-types.js.map +1 -0
  51. package/dist/core/security/access-control.d.ts +32 -0
  52. package/dist/core/security/access-control.d.ts.map +1 -0
  53. package/dist/core/security/access-control.js +244 -0
  54. package/dist/core/security/access-control.js.map +1 -0
  55. package/dist/core/security/config-loader.d.ts +20 -0
  56. package/dist/core/security/config-loader.d.ts.map +1 -0
  57. package/dist/core/security/config-loader.js +227 -0
  58. package/dist/core/security/config-loader.js.map +1 -0
  59. package/dist/core/security/types.d.ts +64 -0
  60. package/dist/core/security/types.d.ts.map +1 -0
  61. package/dist/core/security/types.js +28 -0
  62. package/dist/core/security/types.js.map +1 -0
  63. package/dist/core/sql-parser.d.ts +23 -0
  64. package/dist/core/sql-parser.d.ts.map +1 -0
  65. package/dist/core/sql-parser.js +460 -0
  66. package/dist/core/sql-parser.js.map +1 -0
  67. package/dist/core/sql-parser.test.d.ts +2 -0
  68. package/dist/core/sql-parser.test.d.ts.map +1 -0
  69. package/dist/core/sql-parser.test.js +21 -0
  70. package/dist/core/sql-parser.test.js.map +1 -0
  71. package/dist/handlers/accessible-schema.d.ts +53 -0
  72. package/dist/handlers/accessible-schema.d.ts.map +1 -0
  73. package/dist/handlers/accessible-schema.js +192 -0
  74. package/dist/handlers/accessible-schema.js.map +1 -0
  75. package/dist/handlers/data.d.ts +17 -0
  76. package/dist/handlers/data.d.ts.map +1 -0
  77. package/dist/handlers/data.js +30 -0
  78. package/dist/handlers/data.js.map +1 -0
  79. package/dist/handlers/relationships.d.ts +14 -0
  80. package/dist/handlers/relationships.d.ts.map +1 -0
  81. package/dist/handlers/relationships.js +77 -0
  82. package/dist/handlers/relationships.js.map +1 -0
  83. package/dist/handlers/schema.d.ts +14 -0
  84. package/dist/handlers/schema.d.ts.map +1 -0
  85. package/dist/handlers/schema.js +104 -0
  86. package/dist/handlers/schema.js.map +1 -0
  87. package/dist/handlers/search.d.ts +14 -0
  88. package/dist/handlers/search.d.ts.map +1 -0
  89. package/dist/handlers/search.js +18 -0
  90. package/dist/handlers/search.js.map +1 -0
  91. package/dist/handlers/validation.d.ts +32 -0
  92. package/dist/handlers/validation.d.ts.map +1 -0
  93. package/dist/handlers/validation.js +116 -0
  94. package/dist/handlers/validation.js.map +1 -0
  95. package/dist/index.d.ts +3 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +294 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/mysql/connection.d.ts +15 -0
  100. package/dist/mysql/connection.d.ts.map +1 -0
  101. package/dist/mysql/connection.js +57 -0
  102. package/dist/mysql/connection.js.map +1 -0
  103. package/dist/mysql/identifiers.d.ts +3 -0
  104. package/dist/mysql/identifiers.d.ts.map +1 -0
  105. package/dist/mysql/identifiers.js +10 -0
  106. package/dist/mysql/identifiers.js.map +1 -0
  107. package/dist/mysql/identifiers.test.d.ts +2 -0
  108. package/dist/mysql/identifiers.test.d.ts.map +1 -0
  109. package/dist/mysql/identifiers.test.js +11 -0
  110. package/dist/mysql/identifiers.test.js.map +1 -0
  111. package/dist/mysql/queries.d.ts +49 -0
  112. package/dist/mysql/queries.d.ts.map +1 -0
  113. package/dist/mysql/queries.js +206 -0
  114. package/dist/mysql/queries.js.map +1 -0
  115. package/docs/clients/claude-code.md +28 -0
  116. package/docs/clients/claude-desktop.md +35 -0
  117. package/docs/clients/codex.md +28 -0
  118. package/docs/clients/cursor.md +26 -0
  119. package/docs/clients/opencode.md +35 -0
  120. package/docs/clients/vscode.md +25 -0
  121. package/package.json +49 -0
  122. package/query-access.example.json +21 -0
  123. package/src/cli.ts +221 -0
  124. package/src/core/cache.ts +41 -0
  125. package/src/core/config.ts +97 -0
  126. package/src/core/database-url.test.ts +28 -0
  127. package/src/core/database-url.ts +47 -0
  128. package/src/core/logger.ts +24 -0
  129. package/src/core/mcp.ts +23 -0
  130. package/src/core/query-safety.test.ts +36 -0
  131. package/src/core/query-safety.ts +63 -0
  132. package/src/core/schema-types.ts +58 -0
  133. package/src/core/security/access-control.ts +321 -0
  134. package/src/core/security/config-loader.ts +315 -0
  135. package/src/core/security/types.ts +114 -0
  136. package/src/core/sql-parser.test.ts +37 -0
  137. package/src/core/sql-parser.ts +572 -0
  138. package/src/handlers/accessible-schema.ts +314 -0
  139. package/src/handlers/data.ts +66 -0
  140. package/src/handlers/relationships.ts +114 -0
  141. package/src/handlers/schema.ts +154 -0
  142. package/src/handlers/search.ts +34 -0
  143. package/src/handlers/validation.ts +165 -0
  144. package/src/index.ts +337 -0
  145. package/src/mysql/connection.ts +68 -0
  146. package/src/mysql/identifiers.test.ts +12 -0
  147. package/src/mysql/identifiers.ts +10 -0
  148. package/src/mysql/queries.ts +285 -0
  149. package/tsconfig.json +24 -0
@@ -0,0 +1,165 @@
1
+ import { resolveDatabase, resolveSchema } from '../core/config.js';
2
+ import { logger } from '../core/logger.js';
3
+ import { db } from '../mysql/connection.js';
4
+
5
+ export interface ValidationResult {
6
+ exists: boolean;
7
+ actualName?: string;
8
+ suggestions?: string[];
9
+ message: string;
10
+ }
11
+
12
+ export interface DatabaseValidation extends ValidationResult {
13
+ databases?: string[];
14
+ }
15
+
16
+ export interface SchemaValidation extends ValidationResult {
17
+ schemas?: string[];
18
+ }
19
+
20
+ export interface TableValidation extends ValidationResult {
21
+ tables?: Array<{ schema: string; table: string; fullName: string; type?: string; rowCount?: number | null }>;
22
+ }
23
+
24
+ export async function validateDatabase(database?: string): Promise<DatabaseValidation> {
25
+ const actualName = resolveDatabase(database);
26
+ return {
27
+ exists: true,
28
+ actualName,
29
+ message: database && database !== actualName
30
+ ? `Database found (case mismatch): '${actualName}' (you provided '${database}')`
31
+ : `Database '${actualName}' is configured for this server`,
32
+ };
33
+ }
34
+
35
+ export async function validateSchema(database: string | undefined, schema?: string): Promise<SchemaValidation> {
36
+ const actualDatabase = resolveDatabase(database);
37
+ const actualSchema = resolveSchema(schema);
38
+ return {
39
+ exists: true,
40
+ actualName: actualSchema,
41
+ message: `Schema '${actualSchema}' is available in database '${actualDatabase}'`,
42
+ };
43
+ }
44
+
45
+ export async function validateTable(
46
+ database: string | undefined,
47
+ table: string,
48
+ schema?: string,
49
+ ): Promise<TableValidation> {
50
+ const actualDatabase = resolveDatabase(database);
51
+ resolveSchema(schema);
52
+
53
+ try {
54
+ const matches = await db.query<any>(
55
+ `
56
+ SELECT
57
+ TABLE_SCHEMA AS schemaName,
58
+ TABLE_NAME AS tableName,
59
+ CONCAT(TABLE_SCHEMA, '.', TABLE_NAME) AS fullName,
60
+ TABLE_TYPE AS objectType,
61
+ TABLE_ROWS AS rowCount
62
+ FROM information_schema.TABLES
63
+ WHERE TABLE_SCHEMA = :database
64
+ AND LOWER(TABLE_NAME) = LOWER(:table)
65
+ AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')
66
+ ORDER BY TABLE_NAME
67
+ `,
68
+ { database: actualDatabase, table },
69
+ );
70
+
71
+ if (matches.length === 1) {
72
+ const match = matches[0];
73
+ return {
74
+ exists: true,
75
+ actualName: match.fullName,
76
+ message: match.tableName === table
77
+ ? `Table '${match.fullName}' exists`
78
+ : `Table found (case mismatch): '${match.fullName}' (you provided '${table}')`,
79
+ };
80
+ }
81
+
82
+ const suggestions = await db.query<any>(
83
+ `
84
+ SELECT
85
+ TABLE_SCHEMA AS schemaName,
86
+ TABLE_NAME AS tableName,
87
+ CONCAT(TABLE_SCHEMA, '.', TABLE_NAME) AS fullName,
88
+ TABLE_TYPE AS objectType,
89
+ TABLE_ROWS AS rowCount
90
+ FROM information_schema.TABLES
91
+ WHERE TABLE_SCHEMA = :database
92
+ AND LOWER(TABLE_NAME) LIKE CONCAT('%', LOWER(:table), '%')
93
+ AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')
94
+ ORDER BY TABLE_NAME
95
+ LIMIT 10
96
+ `,
97
+ { database: actualDatabase, table },
98
+ );
99
+
100
+ const tables = suggestions.map((row) => ({
101
+ schema: row.schemaName,
102
+ table: row.tableName,
103
+ fullName: row.fullName,
104
+ type: row.objectType,
105
+ rowCount: row.rowCount,
106
+ }));
107
+
108
+ return {
109
+ exists: false,
110
+ tables,
111
+ suggestions: tables.slice(0, 5).map((row) => row.fullName),
112
+ message: tables.length
113
+ ? `Table '${table}' not found in database '${actualDatabase}'. Did you mean: ${tables.slice(0, 5).map((row) => row.fullName).join(', ')}?`
114
+ : `Table '${table}' not found in database '${actualDatabase}'. No similar tables found.`,
115
+ };
116
+ } catch (error) {
117
+ logger.error('Table validation failed:', error);
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ export async function validateDatabaseObject(
123
+ database?: string,
124
+ table?: string,
125
+ schema?: string,
126
+ ): Promise<{
127
+ valid: boolean;
128
+ database: DatabaseValidation;
129
+ schema?: SchemaValidation;
130
+ table?: TableValidation;
131
+ message: string;
132
+ }> {
133
+ const databaseValidation = await validateDatabase(database);
134
+ const schemaValidation = schema
135
+ ? await validateSchema(databaseValidation.actualName, schema)
136
+ : undefined;
137
+
138
+ if (table) {
139
+ const tableValidation = await validateTable(databaseValidation.actualName, table, schemaValidation?.actualName);
140
+ if (!tableValidation.exists) {
141
+ return {
142
+ valid: false,
143
+ database: databaseValidation,
144
+ schema: schemaValidation,
145
+ table: tableValidation,
146
+ message: tableValidation.message,
147
+ };
148
+ }
149
+
150
+ return {
151
+ valid: true,
152
+ database: databaseValidation,
153
+ schema: schemaValidation,
154
+ table: tableValidation,
155
+ message: `Validation successful: ${tableValidation.actualName}`,
156
+ };
157
+ }
158
+
159
+ return {
160
+ valid: true,
161
+ database: databaseValidation,
162
+ schema: schemaValidation,
163
+ message: 'Validation successful',
164
+ };
165
+ }
package/src/index.ts ADDED
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ Tool,
9
+ } from '@modelcontextprotocol/sdk/types.js';
10
+ import { appConfig } from './core/config.js';
11
+ import { errorResult, textResult } from './core/mcp.js';
12
+ import { logger } from './core/logger.js';
13
+ import {
14
+ initAccessControl,
15
+ loadAccessControlConfig,
16
+ } from './core/security/access-control.js';
17
+ import { executeQuery } from './handlers/data.js';
18
+ import {
19
+ getAccessibleSchema,
20
+ getAccessibleTableInfo,
21
+ } from './handlers/accessible-schema.js';
22
+ import { getRelationships } from './handlers/relationships.js';
23
+ import { getSchema, getTableInfo } from './handlers/schema.js';
24
+ import { findTables, searchObjects } from './handlers/search.js';
25
+ import { validateDatabaseObject } from './handlers/validation.js';
26
+ import { db } from './mysql/connection.js';
27
+
28
+ const databaseProperty = {
29
+ type: 'string',
30
+ description:
31
+ 'Optional compatibility field. If provided, it must match the configured DB_NAME.',
32
+ };
33
+
34
+ const schemaProperty = {
35
+ type: 'string',
36
+ description:
37
+ 'Optional compatibility field. MySQL uses DB_NAME as the schema for this server.',
38
+ };
39
+
40
+ const tools: Tool[] = [
41
+ {
42
+ name: 'get_schema',
43
+ description:
44
+ 'Retrieves MySQL schema information for one or more tables. Returns columns, data types, primary keys, foreign keys, indexes, and optional table statistics.',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ database: databaseProperty,
49
+ tables: {
50
+ type: 'array',
51
+ items: { type: 'string' },
52
+ description:
53
+ 'Table names to retrieve. Leave empty to get all tables in the configured database.',
54
+ },
55
+ schema: schemaProperty,
56
+ includeRelationships: {
57
+ type: 'boolean',
58
+ description: 'Include foreign key relationships (default: true)',
59
+ default: true,
60
+ },
61
+ includeStatistics: {
62
+ type: 'boolean',
63
+ description: 'Include approximate table row/size statistics (default: false)',
64
+ default: false,
65
+ },
66
+ },
67
+ required: [],
68
+ },
69
+ },
70
+ {
71
+ name: 'get_table_info',
72
+ description:
73
+ 'Gets detailed metadata for a single MySQL table or view. Use get_schema for batch table metadata.',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ database: databaseProperty,
78
+ table: {
79
+ type: 'string',
80
+ description: 'Table or view name, for example "customers"',
81
+ },
82
+ schema: schemaProperty,
83
+ },
84
+ required: ['table'],
85
+ },
86
+ },
87
+ {
88
+ name: 'find_tables',
89
+ description:
90
+ 'Searches MySQL tables/views by name pattern or by containing a matching column. Supports * and ? wildcards.',
91
+ inputSchema: {
92
+ type: 'object',
93
+ properties: {
94
+ database: databaseProperty,
95
+ pattern: {
96
+ type: 'string',
97
+ description: 'Table name pattern, for example "*customer*" or "order_*".',
98
+ },
99
+ hasColumn: {
100
+ type: 'string',
101
+ description: 'Column name pattern, for example "*email*" or "created_at".',
102
+ },
103
+ schema: schemaProperty,
104
+ },
105
+ required: [],
106
+ },
107
+ },
108
+ {
109
+ name: 'search_objects',
110
+ description:
111
+ 'Searches MySQL table and column names. Returns matching table and column references.',
112
+ inputSchema: {
113
+ type: 'object',
114
+ properties: {
115
+ database: databaseProperty,
116
+ search: {
117
+ type: 'string',
118
+ description: 'Search string or wildcard pattern, for example "order" or "*email*".',
119
+ },
120
+ schema: schemaProperty,
121
+ type: {
122
+ type: 'string',
123
+ enum: ['table', 'column'],
124
+ description: 'Optional object type filter.',
125
+ },
126
+ },
127
+ required: ['search'],
128
+ },
129
+ },
130
+ {
131
+ name: 'get_relationships',
132
+ description:
133
+ 'Maps foreign key relationships for JOIN path discovery between MySQL tables.',
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: {
137
+ database: databaseProperty,
138
+ fromTable: {
139
+ type: 'string',
140
+ description: 'Source table name',
141
+ },
142
+ toTable: {
143
+ type: 'string',
144
+ description:
145
+ 'Target table name. If omitted, returns direct relationships for fromTable.',
146
+ },
147
+ maxDepth: {
148
+ type: 'number',
149
+ description: 'Maximum relationship traversal depth (default: 2)',
150
+ default: 2,
151
+ },
152
+ schema: schemaProperty,
153
+ },
154
+ required: ['fromTable'],
155
+ },
156
+ },
157
+ {
158
+ name: 'validate_objects',
159
+ description:
160
+ 'Validates the configured database and optional MySQL table names, with suggestions for close table-name matches.',
161
+ inputSchema: {
162
+ type: 'object',
163
+ properties: {
164
+ database: databaseProperty,
165
+ table: {
166
+ type: 'string',
167
+ description: 'Single table name to validate.',
168
+ },
169
+ tables: {
170
+ type: 'array',
171
+ items: { type: 'string' },
172
+ description: 'Multiple table names to validate.',
173
+ },
174
+ schema: schemaProperty,
175
+ },
176
+ required: [],
177
+ },
178
+ },
179
+ {
180
+ name: 'get_accessible_schema',
181
+ description:
182
+ 'Shows tables and columns accessible for SELECT queries based on QUERY_ACCESS_CONFIG.',
183
+ inputSchema: {
184
+ type: 'object',
185
+ properties: {
186
+ database: databaseProperty,
187
+ schema: schemaProperty,
188
+ },
189
+ required: [],
190
+ },
191
+ },
192
+ {
193
+ name: 'get_accessible_table_info',
194
+ description:
195
+ 'Shows table columns with access status according to QUERY_ACCESS_CONFIG.',
196
+ inputSchema: {
197
+ type: 'object',
198
+ properties: {
199
+ database: databaseProperty,
200
+ table: {
201
+ type: 'string',
202
+ description: 'Table name to check.',
203
+ },
204
+ schema: schemaProperty,
205
+ },
206
+ required: ['table'],
207
+ },
208
+ },
209
+ ...(!appConfig.server.schemaOnlyMode
210
+ ? [
211
+ {
212
+ name: 'execute_query',
213
+ description:
214
+ 'Executes a guarded read-only MySQL SELECT query. Queries are validated, access-controlled, and automatically limited with LIMIT.',
215
+ inputSchema: {
216
+ type: 'object' as const,
217
+ properties: {
218
+ database: databaseProperty,
219
+ query: {
220
+ type: 'string',
221
+ description:
222
+ 'Read-only SELECT query. Supports joins, CTEs, subqueries, WHERE, GROUP BY, HAVING, ORDER BY, and LIMIT.',
223
+ },
224
+ parameters: {
225
+ type: 'object',
226
+ description:
227
+ 'Optional named parameters for mysql2 named placeholders, for example {"id": 123}.',
228
+ },
229
+ },
230
+ required: ['query'],
231
+ },
232
+ },
233
+ ]
234
+ : []),
235
+ ];
236
+
237
+ const server = new Server(
238
+ {
239
+ name: appConfig.server.name,
240
+ version: appConfig.server.version,
241
+ },
242
+ {
243
+ capabilities: {
244
+ tools: {},
245
+ },
246
+ },
247
+ );
248
+
249
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
250
+
251
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
252
+ const { name, arguments: args = {} } = request.params;
253
+
254
+ try {
255
+ logger.info(`Tool called: ${name}`, args);
256
+
257
+ switch (name) {
258
+ case 'get_schema':
259
+ return textResult(await getSchema(args as any));
260
+ case 'get_table_info':
261
+ return textResult(await getTableInfo(args as any));
262
+ case 'find_tables':
263
+ return textResult(await findTables(args as any));
264
+ case 'search_objects':
265
+ return textResult(await searchObjects(args as any));
266
+ case 'get_relationships':
267
+ return textResult(await getRelationships(args as any));
268
+ case 'validate_objects': {
269
+ const { database, table, tables, schema } = args as any;
270
+ if (Array.isArray(tables)) {
271
+ return textResult(
272
+ await Promise.all(
273
+ tables.map((candidate: string) =>
274
+ validateDatabaseObject(database, candidate, schema),
275
+ ),
276
+ ),
277
+ );
278
+ }
279
+ return textResult(await validateDatabaseObject(database, table, schema));
280
+ }
281
+ case 'get_accessible_schema':
282
+ return textResult(await getAccessibleSchema(args as any));
283
+ case 'get_accessible_table_info':
284
+ return textResult(await getAccessibleTableInfo(args as any));
285
+ case 'execute_query':
286
+ if (appConfig.server.schemaOnlyMode) {
287
+ throw new Error('Data query operations are disabled because SCHEMA_ONLY_MODE=true.');
288
+ }
289
+ return textResult(await executeQuery(args as any));
290
+ default:
291
+ throw new Error(`Unknown tool: ${name}`);
292
+ }
293
+ } catch (error) {
294
+ logger.error(`Error executing tool ${name}:`, error);
295
+ return errorResult(error);
296
+ }
297
+ });
298
+
299
+ async function main(): Promise<void> {
300
+ try {
301
+ await db.connect();
302
+
303
+ if (appConfig.server.schemaOnlyMode) {
304
+ logger.info('SCHEMA_ONLY_MODE enabled - execute_query is disabled');
305
+ } else if (process.env.QUERY_ACCESS_CONFIG) {
306
+ try {
307
+ initAccessControl(loadAccessControlConfig());
308
+ logger.info('Access control enabled for execute_query');
309
+ } catch (error) {
310
+ logger.error('Failed to load access control config:', error);
311
+ }
312
+ } else {
313
+ logger.warn('QUERY_ACCESS_CONFIG not set - execute_query will be blocked');
314
+ }
315
+
316
+ const transport = new StdioServerTransport();
317
+ await server.connect(transport);
318
+ logger.info(`${appConfig.server.name} v${appConfig.server.version} started`);
319
+ } catch (error) {
320
+ logger.error('Failed to start server:', error);
321
+ process.exit(1);
322
+ }
323
+ }
324
+
325
+ process.on('SIGINT', async () => {
326
+ logger.info('Shutting down...');
327
+ await db.close();
328
+ process.exit(0);
329
+ });
330
+
331
+ process.on('SIGTERM', async () => {
332
+ logger.info('Shutting down...');
333
+ await db.close();
334
+ process.exit(0);
335
+ });
336
+
337
+ main();
@@ -0,0 +1,68 @@
1
+ import mysql, { Pool, RowDataPacket } from 'mysql2/promise';
2
+ import { appConfig } from '../core/config.js';
3
+ import { logger } from '../core/logger.js';
4
+
5
+ class MySqlConnection {
6
+ private static instance: MySqlConnection;
7
+ private pool: Pool | null = null;
8
+
9
+ private constructor() {}
10
+
11
+ static getInstance(): MySqlConnection {
12
+ if (!MySqlConnection.instance) {
13
+ MySqlConnection.instance = new MySqlConnection();
14
+ }
15
+ return MySqlConnection.instance;
16
+ }
17
+
18
+ private createPool(): Pool {
19
+ logger.info(`Creating MySQL pool for ${appConfig.db.host}:${appConfig.db.port}/${appConfig.db.name}`);
20
+ return mysql.createPool({
21
+ host: appConfig.db.host,
22
+ port: appConfig.db.port,
23
+ database: appConfig.db.name,
24
+ user: appConfig.db.user,
25
+ password: appConfig.db.password,
26
+ ssl: appConfig.db.ssl ? { rejectUnauthorized: true } : undefined,
27
+ namedPlaceholders: true,
28
+ waitForConnections: true,
29
+ connectionLimit: 10,
30
+ maxIdle: 10,
31
+ idleTimeout: 30000,
32
+ queueLimit: 0,
33
+ });
34
+ }
35
+
36
+ async connect(): Promise<Pool> {
37
+ if (!this.pool) {
38
+ this.pool = this.createPool();
39
+ await this.pool.query('SELECT 1');
40
+ logger.info('Connected to MySQL successfully');
41
+ }
42
+ return this.pool;
43
+ }
44
+
45
+ async query<T extends RowDataPacket = RowDataPacket>(
46
+ sql: string,
47
+ params?: Record<string, unknown>,
48
+ ): Promise<T[]> {
49
+ const pool = await this.connect();
50
+ logger.debug(`Executing query: ${sql}`);
51
+ const [rows] = await pool.query<T[]>(sql, (params || {}) as any);
52
+ return rows;
53
+ }
54
+
55
+ async close(): Promise<void> {
56
+ if (this.pool) {
57
+ await this.pool.end();
58
+ this.pool = null;
59
+ logger.info('MySQL connection pool closed');
60
+ }
61
+ }
62
+
63
+ isConnected(): boolean {
64
+ return this.pool !== null;
65
+ }
66
+ }
67
+
68
+ export const db = MySqlConnection.getInstance();
@@ -0,0 +1,12 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { likePattern, quoteIdentifier } from './identifiers.js';
4
+
5
+ test('quoteIdentifier escapes backticks', () => {
6
+ assert.equal(quoteIdentifier('order`items'), '`order``items`');
7
+ });
8
+
9
+ test('likePattern converts simple wildcards', () => {
10
+ assert.equal(likePattern('*customer?'), '%customer_');
11
+ assert.equal(likePattern(undefined), null);
12
+ });
@@ -0,0 +1,10 @@
1
+ export function quoteIdentifier(identifier: string): string {
2
+ return `\`${identifier.replace(/`/g, '``')}\``;
3
+ }
4
+
5
+ export function likePattern(pattern?: string): string | null {
6
+ if (!pattern) {
7
+ return null;
8
+ }
9
+ return pattern.replace(/\*/g, '%').replace(/\?/g, '_');
10
+ }