@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,206 @@
1
+ import { appConfig } from '../core/config.js';
2
+ import { db } from './connection.js';
3
+ import { likePattern } from './identifiers.js';
4
+ export async function listTables(tableNames) {
5
+ const tableFilter = tableNames?.length
6
+ ? 'AND t.TABLE_NAME IN (:tableNames)'
7
+ : '';
8
+ return db.query(`
9
+ SELECT
10
+ t.TABLE_SCHEMA AS schemaName,
11
+ t.TABLE_NAME AS tableName,
12
+ t.TABLE_TYPE AS tableType,
13
+ t.CREATE_TIME AS createDate,
14
+ t.TABLE_ROWS AS rowCount
15
+ FROM information_schema.TABLES t
16
+ WHERE t.TABLE_SCHEMA = :database
17
+ AND t.TABLE_TYPE IN ('BASE TABLE', 'VIEW')
18
+ ${tableFilter}
19
+ ORDER BY t.TABLE_NAME
20
+ `, { database: appConfig.db.name, tableNames });
21
+ }
22
+ export async function getColumns(tableNames) {
23
+ const tableFilter = tableNames?.length
24
+ ? 'AND c.TABLE_NAME IN (:tableNames)'
25
+ : '';
26
+ return db.query(`
27
+ SELECT
28
+ c.COLUMN_NAME AS name,
29
+ c.ORDINAL_POSITION AS ordinal,
30
+ c.COLUMN_TYPE AS dataType,
31
+ CASE WHEN c.IS_NULLABLE = 'YES' THEN true ELSE false END AS nullable,
32
+ CASE WHEN c.EXTRA LIKE '%auto_increment%' THEN true ELSE false END AS isIdentity,
33
+ CASE WHEN c.EXTRA LIKE '%VIRTUAL GENERATED%' OR c.EXTRA LIKE '%STORED GENERATED%' THEN true ELSE false END AS isComputed,
34
+ c.COLUMN_DEFAULT AS defaultValue,
35
+ c.COLUMN_COMMENT AS description,
36
+ CASE WHEN kcu.CONSTRAINT_NAME = 'PRIMARY' THEN true ELSE false END AS isPrimaryKey,
37
+ CASE WHEN fk.CONSTRAINT_NAME IS NOT NULL THEN true ELSE false END AS isForeignKey,
38
+ c.TABLE_SCHEMA AS \`schema\`,
39
+ c.TABLE_NAME AS tableName
40
+ FROM information_schema.COLUMNS c
41
+ LEFT JOIN information_schema.KEY_COLUMN_USAGE kcu
42
+ ON kcu.TABLE_SCHEMA = c.TABLE_SCHEMA
43
+ AND kcu.TABLE_NAME = c.TABLE_NAME
44
+ AND kcu.COLUMN_NAME = c.COLUMN_NAME
45
+ AND kcu.CONSTRAINT_NAME = 'PRIMARY'
46
+ LEFT JOIN information_schema.KEY_COLUMN_USAGE fk
47
+ ON fk.TABLE_SCHEMA = c.TABLE_SCHEMA
48
+ AND fk.TABLE_NAME = c.TABLE_NAME
49
+ AND fk.COLUMN_NAME = c.COLUMN_NAME
50
+ AND fk.REFERENCED_TABLE_NAME IS NOT NULL
51
+ WHERE c.TABLE_SCHEMA = :database
52
+ ${tableFilter}
53
+ ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION
54
+ `, { database: appConfig.db.name, tableNames });
55
+ }
56
+ export async function getPrimaryKeys(tableNames) {
57
+ const tableFilter = tableNames?.length
58
+ ? 'AND kcu.TABLE_NAME IN (:tableNames)'
59
+ : '';
60
+ return db.query(`
61
+ SELECT
62
+ kcu.TABLE_NAME AS tableName,
63
+ kcu.CONSTRAINT_NAME AS constraintName,
64
+ GROUP_CONCAT(kcu.COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION SEPARATOR ',') AS columns
65
+ FROM information_schema.KEY_COLUMN_USAGE kcu
66
+ WHERE kcu.TABLE_SCHEMA = :database
67
+ AND kcu.CONSTRAINT_NAME = 'PRIMARY'
68
+ ${tableFilter}
69
+ GROUP BY kcu.TABLE_NAME, kcu.CONSTRAINT_NAME
70
+ `, { database: appConfig.db.name, tableNames });
71
+ }
72
+ export async function getForeignKeys(tableNames) {
73
+ const tableFilter = tableNames?.length
74
+ ? 'AND kcu.TABLE_NAME IN (:tableNames)'
75
+ : '';
76
+ return db.query(`
77
+ SELECT
78
+ kcu.TABLE_NAME AS tableName,
79
+ kcu.CONSTRAINT_NAME AS constraintName,
80
+ kcu.TABLE_SCHEMA AS fromSchema,
81
+ kcu.TABLE_NAME AS fromTable,
82
+ GROUP_CONCAT(kcu.COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION SEPARATOR ',') AS fromColumns,
83
+ kcu.REFERENCED_TABLE_SCHEMA AS toSchema,
84
+ kcu.REFERENCED_TABLE_NAME AS toTable,
85
+ GROUP_CONCAT(kcu.REFERENCED_COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION SEPARATOR ',') AS toColumns,
86
+ rc.DELETE_RULE AS onDelete,
87
+ rc.UPDATE_RULE AS onUpdate
88
+ FROM information_schema.KEY_COLUMN_USAGE kcu
89
+ JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
90
+ ON rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
91
+ AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
92
+ WHERE kcu.TABLE_SCHEMA = :database
93
+ AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
94
+ ${tableFilter}
95
+ GROUP BY
96
+ kcu.TABLE_NAME,
97
+ kcu.CONSTRAINT_NAME,
98
+ kcu.TABLE_SCHEMA,
99
+ kcu.REFERENCED_TABLE_SCHEMA,
100
+ kcu.REFERENCED_TABLE_NAME,
101
+ rc.DELETE_RULE,
102
+ rc.UPDATE_RULE
103
+ `, { database: appConfig.db.name, tableNames });
104
+ }
105
+ export async function getIndexes(tableNames) {
106
+ const tableFilter = tableNames?.length
107
+ ? 'AND s.TABLE_NAME IN (:tableNames)'
108
+ : '';
109
+ return db.query(`
110
+ SELECT
111
+ s.TABLE_NAME AS tableName,
112
+ s.INDEX_NAME AS name,
113
+ s.INDEX_TYPE AS type,
114
+ CASE WHEN s.NON_UNIQUE = 0 THEN true ELSE false END AS isUnique,
115
+ CASE WHEN s.INDEX_NAME = 'PRIMARY' THEN true ELSE false END AS isPrimaryKey,
116
+ GROUP_CONCAT(s.COLUMN_NAME ORDER BY s.SEQ_IN_INDEX SEPARATOR ',') AS columns
117
+ FROM information_schema.STATISTICS s
118
+ WHERE s.TABLE_SCHEMA = :database
119
+ ${tableFilter}
120
+ GROUP BY s.TABLE_NAME, s.INDEX_NAME, s.INDEX_TYPE, s.NON_UNIQUE
121
+ `, { database: appConfig.db.name, tableNames });
122
+ }
123
+ export async function getStatistics(tableNames) {
124
+ const tableFilter = tableNames?.length
125
+ ? 'AND t.TABLE_NAME IN (:tableNames)'
126
+ : '';
127
+ return db.query(`
128
+ SELECT
129
+ t.TABLE_NAME AS tableName,
130
+ t.TABLE_ROWS AS rowCount,
131
+ ROUND(((t.DATA_LENGTH + t.INDEX_LENGTH) / 1024), 0) AS totalSizeKB,
132
+ ROUND((t.DATA_LENGTH / 1024), 0) AS usedSizeKB
133
+ FROM information_schema.TABLES t
134
+ WHERE t.TABLE_SCHEMA = :database
135
+ ${tableFilter}
136
+ `, { database: appConfig.db.name, tableNames });
137
+ }
138
+ export async function findTables(pattern, hasColumn) {
139
+ const params = {
140
+ database: appConfig.db.name,
141
+ pattern: likePattern(pattern),
142
+ hasColumn: likePattern(hasColumn),
143
+ };
144
+ return db.query(`
145
+ SELECT DISTINCT
146
+ t.TABLE_SCHEMA AS schemaName,
147
+ t.TABLE_NAME AS tableName,
148
+ t.TABLE_ROWS AS rowCount,
149
+ t.CREATE_TIME AS createDate
150
+ FROM information_schema.TABLES t
151
+ ${hasColumn ? `JOIN information_schema.COLUMNS c
152
+ ON c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME` : ''}
153
+ WHERE t.TABLE_SCHEMA = :database
154
+ AND t.TABLE_TYPE IN ('BASE TABLE', 'VIEW')
155
+ ${pattern ? 'AND t.TABLE_NAME LIKE :pattern' : ''}
156
+ ${hasColumn ? 'AND c.COLUMN_NAME LIKE :hasColumn' : ''}
157
+ ORDER BY t.TABLE_NAME
158
+ LIMIT 100
159
+ `, params);
160
+ }
161
+ export async function searchObjects(search, type) {
162
+ const pattern = likePattern(search) || `%${search}%`;
163
+ const includeTables = !type || type === 'table';
164
+ const includeColumns = !type || type === 'column';
165
+ const parts = [];
166
+ if (includeTables) {
167
+ parts.push(`
168
+ SELECT t.TABLE_SCHEMA AS schemaName, t.TABLE_NAME AS tableName, NULL AS columnName
169
+ FROM information_schema.TABLES t
170
+ WHERE t.TABLE_SCHEMA = :database
171
+ AND t.TABLE_NAME LIKE :pattern`);
172
+ }
173
+ if (includeColumns) {
174
+ parts.push(`
175
+ SELECT c.TABLE_SCHEMA AS schemaName, c.TABLE_NAME AS tableName, c.COLUMN_NAME AS columnName
176
+ FROM information_schema.COLUMNS c
177
+ WHERE c.TABLE_SCHEMA = :database
178
+ AND c.COLUMN_NAME LIKE :pattern`);
179
+ }
180
+ if (parts.length === 0) {
181
+ return [];
182
+ }
183
+ return db.query(`${parts.join('\nUNION ALL\n')}\nORDER BY tableName, columnName\nLIMIT 100`, { database: appConfig.db.name, pattern });
184
+ }
185
+ export async function getRelationships() {
186
+ return db.query(`
187
+ SELECT
188
+ kcu.TABLE_SCHEMA AS fromSchema,
189
+ kcu.TABLE_NAME AS fromTable,
190
+ kcu.COLUMN_NAME AS fromColumn,
191
+ kcu.REFERENCED_TABLE_SCHEMA AS toSchema,
192
+ kcu.REFERENCED_TABLE_NAME AS toTable,
193
+ kcu.REFERENCED_COLUMN_NAME AS toColumn,
194
+ kcu.CONSTRAINT_NAME AS constraintName,
195
+ rc.DELETE_RULE AS deleteAction,
196
+ rc.UPDATE_RULE AS updateAction
197
+ FROM information_schema.KEY_COLUMN_USAGE kcu
198
+ JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
199
+ ON rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
200
+ AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
201
+ WHERE kcu.TABLE_SCHEMA = :database
202
+ AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
203
+ ORDER BY kcu.TABLE_NAME, kcu.ORDINAL_POSITION
204
+ `, { database: appConfig.db.name });
205
+ }
206
+ //# sourceMappingURL=queries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.js","sourceRoot":"","sources":["../../src/mysql/queries.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAQ9C,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAmC/C,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAqB;IACpD,MAAM,WAAW,GAAG,UAAU,EAAE,MAAM;QACpC,CAAC,CAAC,mCAAmC;QACrC,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,EAAE,CAAC,KAAK,CACb;;;;;;;;;;IAUA,WAAW;;CAEd,EACG,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,CAC5C,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAqB;IACpD,MAAM,WAAW,GAAG,UAAU,EAAE,MAAM;QACpC,CAAC,CAAC,mCAAmC;QACrC,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,EAAE,CAAC,KAAK,CACb;;;;;;;;;;;;;;;;;;;;;;;;;;IA0BA,WAAW;;CAEd,EACG,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,CAC5C,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,UAAqB;IACxD,MAAM,WAAW,GAAG,UAAU,EAAE,MAAM;QACpC,CAAC,CAAC,qCAAqC;QACvC,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,EAAE,CAAC,KAAK,CACb;;;;;;;;IAQA,WAAW;;CAEd,EACG,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,CAC5C,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,UAAqB;IACxD,MAAM,WAAW,GAAG,UAAU,EAAE,MAAM;QACpC,CAAC,CAAC,qCAAqC;QACvC,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,EAAE,CAAC,KAAK,CACb;;;;;;;;;;;;;;;;;;IAkBA,WAAW;;;;;;;;;CASd,EACG,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,CAC5C,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAqB;IACpD,MAAM,WAAW,GAAG,UAAU,EAAE,MAAM;QACpC,CAAC,CAAC,mCAAmC;QACrC,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,EAAE,CAAC,KAAK,CACb;;;;;;;;;;IAUA,WAAW;;CAEd,EACG,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,CAC5C,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,UAAqB;IACvD,MAAM,WAAW,GAAG,UAAU,EAAE,MAAM;QACpC,CAAC,CAAC,mCAAmC;QACrC,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,EAAE,CAAC,KAAK,CACb;;;;;;;;IAQA,WAAW;CACd,EACG,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,CAC5C,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAgB,EAAE,SAAkB;IACnE,MAAM,MAAM,GAAG;QACb,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI;QAC3B,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC;QAC7B,SAAS,EAAE,WAAW,CAAC,SAAS,CAAC;KAClC,CAAC;IACF,OAAO,EAAE,CAAC,KAAK,CACb;;;;;;;EAOF,SAAS,CAAC,CAAC,CAAC;qEACuD,CAAC,CAAC,CAAC,EAAE;;;IAGtE,OAAO,CAAC,CAAC,CAAC,gCAAgC,CAAC,CAAC,CAAC,EAAE;IAC/C,SAAS,CAAC,CAAC,CAAC,mCAAmC,CAAC,CAAC,CAAC,EAAE;;;CAGvD,EACG,MAAM,CACP,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc,EAAE,IAAa;IAC/D,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,IAAI,MAAM,GAAG,CAAC;IACrD,MAAM,aAAa,GAAG,CAAC,IAAI,IAAI,IAAI,KAAK,OAAO,CAAC;IAChD,MAAM,cAAc,GAAG,CAAC,IAAI,IAAI,IAAI,KAAK,QAAQ,CAAC;IAClD,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,IAAI,aAAa,EAAE,CAAC;QAClB,KAAK,CAAC,IAAI,CAAC;;;;iCAIkB,CAAC,CAAC;IACjC,CAAC;IAED,IAAI,cAAc,EAAE,CAAC;QACnB,KAAK,CAAC,IAAI,CAAC;;;;kCAImB,CAAC,CAAC;IAClC,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,EAAE,CAAC,KAAK,CACb,GAAG,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,6CAA6C,EAC3E,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,CACzC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,OAAO,EAAE,CAAC,KAAK,CACb;;;;;;;;;;;;;;;;;;CAkBH,EACG,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,CAChC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,28 @@
1
+ # Claude Code
2
+
3
+ Add the server as a local stdio MCP:
4
+
5
+ ```bash
6
+ claude mcp add --transport stdio mysql \
7
+ --env DATABASE_URL=mysql://mcp_reader:password@localhost:3306/app_db \
8
+ -- npx -y @sigma4life/mysql-mcp-server
9
+ ```
10
+
11
+ For read-query support, also pass:
12
+
13
+ ```bash
14
+ --env SCHEMA_ONLY_MODE=false \
15
+ --env QUERY_ACCESS_CONFIG=/absolute/path/to/query-access.json
16
+ ```
17
+
18
+ Check status inside Claude Code:
19
+
20
+ ```text
21
+ /mcp
22
+ ```
23
+
24
+ Try:
25
+
26
+ ```text
27
+ Use mysql to show me the tables in this database.
28
+ ```
@@ -0,0 +1,35 @@
1
+ # Claude Desktop
2
+
3
+ Add this to your Claude Desktop MCP configuration:
4
+
5
+ ```json
6
+ {
7
+ "mcpServers": {
8
+ "mysql": {
9
+ "command": "npx",
10
+ "args": ["-y", "@sigma4life/mysql-mcp-server"],
11
+ "env": {
12
+ "DATABASE_URL": "mysql://mcp_reader:password@localhost:3306/app_db",
13
+ "SCHEMA_ONLY_MODE": "true"
14
+ }
15
+ }
16
+ }
17
+ }
18
+ ```
19
+
20
+ Restart Claude Desktop after editing the config.
21
+
22
+ For read-query support, set:
23
+
24
+ ```json
25
+ {
26
+ "SCHEMA_ONLY_MODE": "false",
27
+ "QUERY_ACCESS_CONFIG": "/absolute/path/to/query-access.json"
28
+ }
29
+ ```
30
+
31
+ Try:
32
+
33
+ ```text
34
+ Show me the schema for customers and orders.
35
+ ```
@@ -0,0 +1,28 @@
1
+ # Codex
2
+
3
+ Add the server as a local stdio MCP:
4
+
5
+ ```bash
6
+ codex mcp add mysql \
7
+ --env DATABASE_URL=mysql://mcp_reader:password@localhost:3306/app_db \
8
+ -- npx -y @sigma4life/mysql-mcp-server
9
+ ```
10
+
11
+ Verify:
12
+
13
+ ```bash
14
+ codex mcp list
15
+ ```
16
+
17
+ For read-query support, also pass:
18
+
19
+ ```bash
20
+ --env SCHEMA_ONLY_MODE=false \
21
+ --env QUERY_ACCESS_CONFIG=/absolute/path/to/query-access.json
22
+ ```
23
+
24
+ Try:
25
+
26
+ ```text
27
+ Use mysql to find tables with an email column.
28
+ ```
@@ -0,0 +1,26 @@
1
+ # Cursor
2
+
3
+ Create or update `~/.cursor/mcp.json`:
4
+
5
+ ```json
6
+ {
7
+ "mcpServers": {
8
+ "mysql": {
9
+ "command": "npx",
10
+ "args": ["-y", "@sigma4life/mysql-mcp-server"],
11
+ "env": {
12
+ "DATABASE_URL": "mysql://mcp_reader:password@localhost:3306/app_db",
13
+ "SCHEMA_ONLY_MODE": "true"
14
+ }
15
+ }
16
+ }
17
+ }
18
+ ```
19
+
20
+ Restart Cursor after editing the config.
21
+
22
+ Try:
23
+
24
+ ```text
25
+ Use mysql to show me the database tables.
26
+ ```
@@ -0,0 +1,35 @@
1
+ # OpenCode
2
+
3
+ Add a local MCP server to `opencode.json`:
4
+
5
+ ```json
6
+ {
7
+ "$schema": "https://opencode.ai/config.json",
8
+ "mcp": {
9
+ "mysql": {
10
+ "type": "local",
11
+ "command": ["npx", "-y", "@sigma4life/mysql-mcp-server"],
12
+ "enabled": true,
13
+ "environment": {
14
+ "DATABASE_URL": "mysql://mcp_reader:password@localhost:3306/app_db",
15
+ "SCHEMA_ONLY_MODE": "true"
16
+ }
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ For read-query support, add:
23
+
24
+ ```json
25
+ {
26
+ "SCHEMA_ONLY_MODE": "false",
27
+ "QUERY_ACCESS_CONFIG": "/absolute/path/to/query-access.json"
28
+ }
29
+ ```
30
+
31
+ Try:
32
+
33
+ ```text
34
+ Use the mysql tool to show relationships from orders.
35
+ ```
@@ -0,0 +1,25 @@
1
+ # VS Code
2
+
3
+ Create `.vscode/mcp.json` in your project:
4
+
5
+ ```json
6
+ {
7
+ "servers": {
8
+ "mysql": {
9
+ "type": "stdio",
10
+ "command": "npx",
11
+ "args": ["-y", "@sigma4life/mysql-mcp-server"],
12
+ "env": {
13
+ "DATABASE_URL": "mysql://mcp_reader:password@localhost:3306/app_db",
14
+ "SCHEMA_ONLY_MODE": "true"
15
+ }
16
+ }
17
+ }
18
+ }
19
+ ```
20
+
21
+ Try:
22
+
23
+ ```text
24
+ Use mysql to inspect the customers table.
25
+ ```
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@sigma4life/mysql-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for MySQL schema introspection and guarded read-only queries",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "mysql-mcp-server": "dist/cli.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/cli.js serve",
14
+ "test": "npm run build && node --test dist/**/*.test.js",
15
+ "lint": "eslint src/**/*.ts",
16
+ "format": "prettier --write src/**/*.ts"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "mysql",
21
+ "database",
22
+ "schema",
23
+ "ai"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.0.4",
29
+ "dotenv": "^16.4.7",
30
+ "mysql2": "^3.12.0",
31
+ "node-sql-parser": "^5.3.6",
32
+ "winston": "^3.17.0",
33
+ "zod": "^3.24.1"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.10.5",
37
+ "@typescript-eslint/eslint-plugin": "^8.20.0",
38
+ "@typescript-eslint/parser": "^8.20.0",
39
+ "eslint": "^9.18.0",
40
+ "prettier": "^3.4.2",
41
+ "typescript": "^5.7.2"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "requireExplicitColumns": true,
3
+ "databases": {
4
+ "app_db": {
5
+ "tables": {
6
+ "mode": "whitelist",
7
+ "list": ["customers", "orders", "products", "order_items"],
8
+ "columnAccess": {
9
+ "customers": {
10
+ "mode": "exclusion",
11
+ "columns": ["password_hash", "api_token"]
12
+ },
13
+ "orders": {
14
+ "mode": "inclusion",
15
+ "columns": ["id", "customer_id", "status", "total", "created_at"]
16
+ }
17
+ }
18
+ }
19
+ }
20
+ }
21
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { maskDatabaseUrl } from './core/database-url.js';
6
+
7
+ const DEFAULT_DATABASE_URL = 'mysql://mcp_reader:change_me@localhost:3306/app_db';
8
+
9
+ function usage(): void {
10
+ console.log(`mysql-mcp-server
11
+
12
+ Usage:
13
+ mysql-mcp-server Start the MCP stdio server
14
+ mysql-mcp-server serve Start the MCP stdio server
15
+ mysql-mcp-server doctor Check configuration and database connectivity
16
+ mysql-mcp-server init [--force] Create starter .env, query-access.json, and client snippets
17
+
18
+ Environment:
19
+ DATABASE_URL=mysql://user:password@host:3306/database
20
+ `);
21
+ }
22
+
23
+ function writeFileIfMissing(filePath: string, content: string, force: boolean): 'created' | 'skipped' {
24
+ if (fs.existsSync(filePath) && !force) {
25
+ return 'skipped';
26
+ }
27
+ fs.writeFileSync(filePath, content);
28
+ return 'created';
29
+ }
30
+
31
+ function envTemplate(): string {
32
+ return `# One-line setup. Individual DB_* variables may override parts of this URL.
33
+ DATABASE_URL=${DEFAULT_DATABASE_URL}
34
+
35
+ # Safer first-run default: schema tools only. Set false to enable execute_query.
36
+ SCHEMA_ONLY_MODE=true
37
+
38
+ # Required only when SCHEMA_ONLY_MODE=false.
39
+ # QUERY_ACCESS_CONFIG=${path.resolve('query-access.json')}
40
+
41
+ MAX_QUERY_ROWS=100
42
+ QUERY_TIMEOUT_MS=30000
43
+ MCP_SERVER_NAME=mysql-mcp-server
44
+ MCP_SERVER_VERSION=1.0.0
45
+ LOG_LEVEL=info
46
+ LOG_FILE=mcp-server.log
47
+ `;
48
+ }
49
+
50
+ function queryAccessTemplate(): string {
51
+ return JSON.stringify(
52
+ {
53
+ requireExplicitColumns: true,
54
+ databases: {
55
+ app_db: {
56
+ tables: {
57
+ mode: 'whitelist',
58
+ list: ['customers', 'orders', 'products', 'order_items'],
59
+ columnAccess: {
60
+ customers: {
61
+ mode: 'exclusion',
62
+ columns: ['password_hash', 'api_token'],
63
+ },
64
+ },
65
+ },
66
+ },
67
+ },
68
+ },
69
+ null,
70
+ 2,
71
+ ) + '\n';
72
+ }
73
+
74
+ function clientSnippetsTemplate(): string {
75
+ return `# mysql-mcp-server client snippets
76
+
77
+ Replace the sample DATABASE_URL with your real MySQL connection string.
78
+
79
+ ## Claude Code
80
+
81
+ \`\`\`bash
82
+ claude mcp add --transport stdio mysql \\
83
+ --env DATABASE_URL=${DEFAULT_DATABASE_URL} \\
84
+ -- npx -y @sigma4life/mysql-mcp-server
85
+ \`\`\`
86
+
87
+ ## Codex
88
+
89
+ \`\`\`bash
90
+ codex mcp add mysql \\
91
+ --env DATABASE_URL=${DEFAULT_DATABASE_URL} \\
92
+ -- npx -y @sigma4life/mysql-mcp-server
93
+ \`\`\`
94
+
95
+ ## Claude Desktop
96
+
97
+ \`\`\`json
98
+ {
99
+ "mcpServers": {
100
+ "mysql": {
101
+ "command": "npx",
102
+ "args": ["-y", "@sigma4life/mysql-mcp-server"],
103
+ "env": {
104
+ "DATABASE_URL": "${DEFAULT_DATABASE_URL}"
105
+ }
106
+ }
107
+ }
108
+ }
109
+ \`\`\`
110
+
111
+ ## OpenCode
112
+
113
+ \`\`\`json
114
+ {
115
+ "$schema": "https://opencode.ai/config.json",
116
+ "mcp": {
117
+ "mysql": {
118
+ "type": "local",
119
+ "command": ["npx", "-y", "@sigma4life/mysql-mcp-server"],
120
+ "enabled": true,
121
+ "environment": {
122
+ "DATABASE_URL": "${DEFAULT_DATABASE_URL}"
123
+ }
124
+ }
125
+ }
126
+ }
127
+ \`\`\`
128
+ `;
129
+ }
130
+
131
+ async function runInit(args: string[]): Promise<void> {
132
+ const force = args.includes('--force');
133
+ const files = [
134
+ ['.env', envTemplate()],
135
+ ['query-access.json', queryAccessTemplate()],
136
+ ['mysql-mcp-clients.md', clientSnippetsTemplate()],
137
+ ] as const;
138
+
139
+ for (const [file, content] of files) {
140
+ const status = writeFileIfMissing(path.resolve(file), content, force);
141
+ console.log(`${status === 'created' ? 'created' : 'skipped'} ${file}`);
142
+ }
143
+
144
+ console.log('\nNext steps:');
145
+ console.log('1. Edit .env with your MySQL credentials.');
146
+ console.log('2. Run: mysql-mcp-server doctor');
147
+ console.log('3. Add one of the snippets from mysql-mcp-clients.md to your MCP client.');
148
+ }
149
+
150
+ async function runDoctor(): Promise<void> {
151
+ console.log('mysql-mcp-server doctor\n');
152
+
153
+ try {
154
+ const { appConfig } = await import('./core/config.js');
155
+ const { db } = await import('./mysql/connection.js');
156
+
157
+ console.log(`server: ${appConfig.server.name} v${appConfig.server.version}`);
158
+ console.log(`database: ${appConfig.db.user}@${appConfig.db.host}:${appConfig.db.port}/${appConfig.db.name}`);
159
+ console.log(`ssl: ${appConfig.db.ssl ? 'enabled' : 'disabled'}`);
160
+ console.log(`schema-only mode: ${appConfig.server.schemaOnlyMode ? 'enabled' : 'disabled'}`);
161
+ if (process.env.DATABASE_URL) {
162
+ console.log(`DATABASE_URL: ${maskDatabaseUrl(process.env.DATABASE_URL)}`);
163
+ }
164
+
165
+ const versionRows = await db.query<any>('SELECT VERSION() AS version');
166
+ console.log(`mysql: ${versionRows[0]?.version || 'connected'}`);
167
+
168
+ const tableRows = await db.query<any>(
169
+ `SELECT COUNT(*) AS tableCount
170
+ FROM information_schema.TABLES
171
+ WHERE TABLE_SCHEMA = :database
172
+ AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')`,
173
+ { database: appConfig.db.name },
174
+ );
175
+ console.log(`tables/views visible: ${tableRows[0]?.tableCount ?? 0}`);
176
+
177
+ if (!appConfig.server.schemaOnlyMode) {
178
+ if (process.env.QUERY_ACCESS_CONFIG) {
179
+ const { loadAccessControlConfig } = await import('./core/security/access-control.js');
180
+ loadAccessControlConfig();
181
+ console.log('query access config: ok');
182
+ } else {
183
+ console.log('query access config: missing; execute_query will be blocked');
184
+ }
185
+ }
186
+
187
+ await db.close();
188
+ console.log('\nDoctor passed.');
189
+ } catch (error) {
190
+ console.error('Doctor failed.');
191
+ console.error(error instanceof Error ? error.message : String(error));
192
+ process.exitCode = 1;
193
+ }
194
+ }
195
+
196
+ async function main(): Promise<void> {
197
+ const [command = 'serve', ...args] = process.argv.slice(2);
198
+
199
+ switch (command) {
200
+ case 'serve':
201
+ await import('./index.js');
202
+ break;
203
+ case 'doctor':
204
+ await runDoctor();
205
+ break;
206
+ case 'init':
207
+ await runInit(args);
208
+ break;
209
+ case '--help':
210
+ case '-h':
211
+ case 'help':
212
+ usage();
213
+ break;
214
+ default:
215
+ console.error(`Unknown command: ${command}\n`);
216
+ usage();
217
+ process.exitCode = 1;
218
+ }
219
+ }
220
+
221
+ main();