@cano721/mysql-mcp-server 0.1.3
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 +285 -0
- package/build/connection.js +128 -0
- package/build/index.js +213 -0
- package/build/types.js +4 -0
- package/build/validators.js +79 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/@cano721/mysql-mcp-server)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# MySQL Database Access MCP Server (@cano721/mysql-mcp-server)
|
|
5
|
+
|
|
6
|
+
This MCP server provides read-only access to MySQL databases. It allows you to:
|
|
7
|
+
|
|
8
|
+
- List available databases
|
|
9
|
+
- List tables in a database
|
|
10
|
+
- Describe table schemas
|
|
11
|
+
- Execute read-only SQL queries
|
|
12
|
+
|
|
13
|
+
## Security Features
|
|
14
|
+
|
|
15
|
+
- **Read-only access**: Only SELECT, SHOW, DESCRIBE, and EXPLAIN statements are allowed
|
|
16
|
+
- **Query validation**: Prevents SQL injection and blocks any data modification attempts
|
|
17
|
+
- **Query timeout**: Prevents long-running queries from consuming resources
|
|
18
|
+
- **Row limit**: Prevents excessive data return
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
### 1. Install using one of these methods:
|
|
23
|
+
|
|
24
|
+
#### Install from NPM
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Install globally
|
|
28
|
+
npm install -g @cano721/mysql-mcp-server
|
|
29
|
+
|
|
30
|
+
# Or install locally in your project
|
|
31
|
+
npm install @cano721/mysql-mcp-server
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
#### Build from Source
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Clone the repository
|
|
38
|
+
git clone https://github.com/cano721/mysql-mcp-server.git
|
|
39
|
+
cd mysql-mcp-server
|
|
40
|
+
|
|
41
|
+
# Install dependencies and build
|
|
42
|
+
npm install
|
|
43
|
+
npm run build
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Install via Smithery
|
|
47
|
+
|
|
48
|
+
To install MySQL Database Access MCP Server for Claude AI automatically via Smithery:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx -y @smithery/cli install @cano721/mysql-mcp-server --client claude
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Configure environment variables
|
|
55
|
+
|
|
56
|
+
The server requires the following environment variables:
|
|
57
|
+
|
|
58
|
+
- `MYSQL_HOST`: Database server hostname
|
|
59
|
+
- `MYSQL_PORT`: Database server port (default: 3306)
|
|
60
|
+
- `MYSQL_USER`: Database username
|
|
61
|
+
- `MYSQL_PASSWORD`: Database password (optional, but recommended for secure connections)
|
|
62
|
+
- `MYSQL_DATABASE`: Default database name (optional)
|
|
63
|
+
|
|
64
|
+
### 3. Add to MCP settings
|
|
65
|
+
|
|
66
|
+
Add the following configuration to your MCP settings file:
|
|
67
|
+
|
|
68
|
+
If you installed via npm (Option 1):
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"mysql": {
|
|
73
|
+
"command": "npx",
|
|
74
|
+
"args": ["@cano721/mysql-mcp-server"],
|
|
75
|
+
"env": {
|
|
76
|
+
"MYSQL_HOST": "your-mysql-host",
|
|
77
|
+
"MYSQL_PORT": "3306",
|
|
78
|
+
"MYSQL_USER": "your-mysql-user",
|
|
79
|
+
"MYSQL_PASSWORD": "your-mysql-password",
|
|
80
|
+
"MYSQL_DATABASE": "your-default-database"
|
|
81
|
+
},
|
|
82
|
+
"disabled": false,
|
|
83
|
+
"autoApprove": []
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If you built from source (Option 2):
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"mcpServers": {
|
|
93
|
+
"mysql": {
|
|
94
|
+
"command": "node",
|
|
95
|
+
"args": ["/path/to/mysql-mcp-server/build/index.js"],
|
|
96
|
+
"env": {
|
|
97
|
+
"MYSQL_HOST": "your-mysql-host",
|
|
98
|
+
"MYSQL_PORT": "3306",
|
|
99
|
+
"MYSQL_USER": "your-mysql-user",
|
|
100
|
+
"MYSQL_PASSWORD": "your-mysql-password",
|
|
101
|
+
"MYSQL_DATABASE": "your-default-database"
|
|
102
|
+
},
|
|
103
|
+
"disabled": false,
|
|
104
|
+
"autoApprove": []
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Available Tools
|
|
111
|
+
|
|
112
|
+
### list_databases
|
|
113
|
+
|
|
114
|
+
Lists all accessible databases on the MySQL server.
|
|
115
|
+
|
|
116
|
+
**Parameters**: None
|
|
117
|
+
|
|
118
|
+
**Example**:
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"server_name": "mysql",
|
|
122
|
+
"tool_name": "list_databases",
|
|
123
|
+
"arguments": {}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### list_tables
|
|
128
|
+
|
|
129
|
+
Lists all tables in a specified database.
|
|
130
|
+
|
|
131
|
+
**Parameters**:
|
|
132
|
+
- `database` (optional): Database name (uses default if not specified)
|
|
133
|
+
|
|
134
|
+
**Example**:
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"server_name": "mysql",
|
|
138
|
+
"tool_name": "list_tables",
|
|
139
|
+
"arguments": {
|
|
140
|
+
"database": "my_database"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### describe_table
|
|
146
|
+
|
|
147
|
+
Shows the schema for a specific table.
|
|
148
|
+
|
|
149
|
+
**Parameters**:
|
|
150
|
+
- `database` (optional): Database name (uses default if not specified)
|
|
151
|
+
- `table` (required): Table name
|
|
152
|
+
|
|
153
|
+
**Example**:
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"server_name": "mysql",
|
|
157
|
+
"tool_name": "describe_table",
|
|
158
|
+
"arguments": {
|
|
159
|
+
"database": "my_database",
|
|
160
|
+
"table": "my_table"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### execute_query
|
|
166
|
+
|
|
167
|
+
Executes a read-only SQL query.
|
|
168
|
+
|
|
169
|
+
**Parameters**:
|
|
170
|
+
- `query` (required): SQL query (only SELECT, SHOW, DESCRIBE, and EXPLAIN statements are allowed)
|
|
171
|
+
- `database` (optional): Database name (uses default if not specified)
|
|
172
|
+
|
|
173
|
+
**Example**:
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"server_name": "mysql",
|
|
177
|
+
"tool_name": "execute_query",
|
|
178
|
+
"arguments": {
|
|
179
|
+
"database": "my_database",
|
|
180
|
+
"query": "SELECT * FROM my_table LIMIT 10"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Advanced Connection Pool Configuration
|
|
186
|
+
|
|
187
|
+
For more control over the MySQL connection pool behavior, you can configure additional parameters:
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"mcpServers": {
|
|
192
|
+
"mysql": {
|
|
193
|
+
"command": "npx",
|
|
194
|
+
"args": ["@cano721/mysql-mcp-server"],
|
|
195
|
+
"env": {
|
|
196
|
+
"MYSQL_HOST": "your-mysql-host",
|
|
197
|
+
"MYSQL_PORT": "3306",
|
|
198
|
+
"MYSQL_USER": "your-mysql-user",
|
|
199
|
+
"MYSQL_PASSWORD": "your-mysql-password",
|
|
200
|
+
"MYSQL_DATABASE": "your-default-database",
|
|
201
|
+
|
|
202
|
+
"MYSQL_CONNECTION_LIMIT": "10",
|
|
203
|
+
"MYSQL_QUEUE_LIMIT": "0",
|
|
204
|
+
"MYSQL_CONNECT_TIMEOUT": "10000",
|
|
205
|
+
"MYSQL_IDLE_TIMEOUT": "60000",
|
|
206
|
+
"MYSQL_MAX_IDLE": "10"
|
|
207
|
+
},
|
|
208
|
+
"disabled": false,
|
|
209
|
+
"autoApprove": []
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
These advanced options allow you to:
|
|
216
|
+
|
|
217
|
+
- `MYSQL_CONNECTION_LIMIT`: Control the maximum number of connections in the pool (default: 10)
|
|
218
|
+
- `MYSQL_QUEUE_LIMIT`: Set the maximum number of connection requests to queue (default: 0, unlimited)
|
|
219
|
+
- `MYSQL_CONNECT_TIMEOUT`: Adjust the connection timeout in milliseconds (default: 10000)
|
|
220
|
+
- `MYSQL_IDLE_TIMEOUT`: Configure how long a connection can be idle before being released (in milliseconds)
|
|
221
|
+
- `MYSQL_MAX_IDLE`: Set the maximum number of idle connections to keep in the pool
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
## Testing
|
|
225
|
+
|
|
226
|
+
The server includes test scripts to verify functionality with your MySQL setup:
|
|
227
|
+
|
|
228
|
+
### 1. Setup Test Database
|
|
229
|
+
|
|
230
|
+
This script creates a test database, table, and sample data:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
# Set your MySQL credentials as environment variables
|
|
234
|
+
export MYSQL_HOST=localhost
|
|
235
|
+
export MYSQL_PORT=3306
|
|
236
|
+
export MYSQL_USER=your_username
|
|
237
|
+
export MYSQL_PASSWORD=your_password
|
|
238
|
+
|
|
239
|
+
# Run the setup script
|
|
240
|
+
npm run test:setup
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### 2. Test MCP Tools
|
|
244
|
+
|
|
245
|
+
This script tests each of the MCP tools against the test database:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
# Set your MySQL credentials as environment variables
|
|
249
|
+
export MYSQL_HOST=localhost
|
|
250
|
+
export MYSQL_PORT=3306
|
|
251
|
+
export MYSQL_USER=your_username
|
|
252
|
+
export MYSQL_PASSWORD=your_password
|
|
253
|
+
export MYSQL_DATABASE=mcp_test_db
|
|
254
|
+
|
|
255
|
+
# Run the tools test script
|
|
256
|
+
npm run test:tools
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 3. Run All Tests
|
|
260
|
+
|
|
261
|
+
To run both setup and tool tests:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
# Set your MySQL credentials as environment variables
|
|
265
|
+
export MYSQL_HOST=localhost
|
|
266
|
+
export MYSQL_PORT=3306
|
|
267
|
+
export MYSQL_USER=your_username
|
|
268
|
+
export MYSQL_PASSWORD=your_password
|
|
269
|
+
|
|
270
|
+
# Run all tests
|
|
271
|
+
npm test
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Troubleshooting
|
|
275
|
+
|
|
276
|
+
If you encounter issues:
|
|
277
|
+
|
|
278
|
+
1. Check the server logs for error messages
|
|
279
|
+
2. Verify your MySQL credentials and connection details
|
|
280
|
+
3. Ensure your MySQL user has appropriate permissions
|
|
281
|
+
4. Check that your query is read-only and properly formatted
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
This project is licensed under the MIT License - see the [LICENSE](../LICENSE) file for details.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MySQL connection management for MCP server
|
|
3
|
+
*/
|
|
4
|
+
import mysql from 'mysql2/promise';
|
|
5
|
+
// Default connection pool configuration
|
|
6
|
+
const DEFAULT_PORT = 3306; // Default MySQL port
|
|
7
|
+
const DEFAULT_TIMEOUT = 10000; // Default connection timeout in milliseconds
|
|
8
|
+
const DEFAULT_CONNECTION_LIMIT = 10; // Default maximum number of connections in the pool
|
|
9
|
+
const DEFAULT_QUEUE_LIMIT = 0; // Default maximum number of connection requests to queue (0 = unlimited)
|
|
10
|
+
const DEFAULT_ROW_LIMIT = 1000; // Default row limit for query results
|
|
11
|
+
/**
|
|
12
|
+
* Create a MySQL connection pool
|
|
13
|
+
*/
|
|
14
|
+
export function createConnectionPool(config) {
|
|
15
|
+
console.error('[Setup] Creating MySQL connection pool');
|
|
16
|
+
try {
|
|
17
|
+
// Create connection options with defaults
|
|
18
|
+
const poolConfig = {
|
|
19
|
+
host: config.host,
|
|
20
|
+
port: config.port,
|
|
21
|
+
user: config.user,
|
|
22
|
+
waitForConnections: true,
|
|
23
|
+
connectionLimit: config.connectionLimit ?? DEFAULT_CONNECTION_LIMIT,
|
|
24
|
+
queueLimit: config.queueLimit ?? DEFAULT_QUEUE_LIMIT,
|
|
25
|
+
connectTimeout: config.connectTimeout ?? DEFAULT_TIMEOUT,
|
|
26
|
+
};
|
|
27
|
+
// Add password if provided
|
|
28
|
+
if (config.password !== undefined) {
|
|
29
|
+
poolConfig.password = config.password;
|
|
30
|
+
}
|
|
31
|
+
// Add database if provided
|
|
32
|
+
if (config.database) {
|
|
33
|
+
poolConfig.database = config.database;
|
|
34
|
+
}
|
|
35
|
+
// Add idleTimeout if provided
|
|
36
|
+
if (config.idleTimeout !== undefined) {
|
|
37
|
+
poolConfig.idleTimeout = config.idleTimeout;
|
|
38
|
+
}
|
|
39
|
+
// Add maxIdle if provided
|
|
40
|
+
if (config.maxIdle !== undefined) {
|
|
41
|
+
poolConfig.maxIdle = config.maxIdle;
|
|
42
|
+
}
|
|
43
|
+
return mysql.createPool(poolConfig);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error('[Error] Failed to create connection pool:', error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Execute a query with error handling and logging
|
|
52
|
+
*/
|
|
53
|
+
export async function executeQuery(pool, sql, params = [], database) {
|
|
54
|
+
console.error(`[Query] Executing: ${sql}`);
|
|
55
|
+
let connection = null;
|
|
56
|
+
try {
|
|
57
|
+
// Get connection from pool
|
|
58
|
+
connection = await pool.getConnection();
|
|
59
|
+
// Use specific database if provided
|
|
60
|
+
if (database) {
|
|
61
|
+
console.error(`[Query] Using database: ${database}`);
|
|
62
|
+
await connection.query(`USE \`${database}\``);
|
|
63
|
+
}
|
|
64
|
+
// Execute query with timeout
|
|
65
|
+
const [rows, fields] = await Promise.race([
|
|
66
|
+
connection.query(sql, params),
|
|
67
|
+
new Promise((_, reject) => {
|
|
68
|
+
setTimeout(() => reject(new Error('Query timeout')), DEFAULT_TIMEOUT);
|
|
69
|
+
}),
|
|
70
|
+
]);
|
|
71
|
+
// Apply row limit if result is an array
|
|
72
|
+
const limitedRows = Array.isArray(rows) && rows.length > DEFAULT_ROW_LIMIT
|
|
73
|
+
? rows.slice(0, DEFAULT_ROW_LIMIT)
|
|
74
|
+
: rows;
|
|
75
|
+
// Log result summary
|
|
76
|
+
console.error(`[Query] Success: ${Array.isArray(rows) ? rows.length : 1} rows returned`);
|
|
77
|
+
return { rows: limitedRows, fields };
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error('[Error] Query execution failed:', error);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
// Release connection back to pool
|
|
85
|
+
if (connection) {
|
|
86
|
+
connection.release();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get MySQL connection configuration from environment variables
|
|
92
|
+
*/
|
|
93
|
+
export function getConfigFromEnv() {
|
|
94
|
+
const host = process.env.MYSQL_HOST;
|
|
95
|
+
const portStr = process.env.MYSQL_PORT;
|
|
96
|
+
const user = process.env.MYSQL_USER;
|
|
97
|
+
const password = process.env.MYSQL_PASSWORD;
|
|
98
|
+
const database = process.env.MYSQL_DATABASE;
|
|
99
|
+
// Connection pool options
|
|
100
|
+
const connectionLimitStr = process.env.MYSQL_CONNECTION_LIMIT;
|
|
101
|
+
const queueLimitStr = process.env.MYSQL_QUEUE_LIMIT;
|
|
102
|
+
const connectTimeoutStr = process.env.MYSQL_CONNECT_TIMEOUT;
|
|
103
|
+
const idleTimeoutStr = process.env.MYSQL_IDLE_TIMEOUT;
|
|
104
|
+
const maxIdleStr = process.env.MYSQL_MAX_IDLE;
|
|
105
|
+
if (!host)
|
|
106
|
+
throw new Error('MYSQL_HOST environment variable is required');
|
|
107
|
+
if (!user)
|
|
108
|
+
throw new Error('MYSQL_USER environment variable is required');
|
|
109
|
+
const port = portStr ? parseInt(portStr, 10) : DEFAULT_PORT;
|
|
110
|
+
// Parse connection pool options (all optional)
|
|
111
|
+
const connectionLimit = connectionLimitStr ? parseInt(connectionLimitStr, 10) : undefined;
|
|
112
|
+
const queueLimit = queueLimitStr ? parseInt(queueLimitStr, 10) : undefined;
|
|
113
|
+
const connectTimeout = connectTimeoutStr ? parseInt(connectTimeoutStr, 10) : undefined;
|
|
114
|
+
const idleTimeout = idleTimeoutStr ? parseInt(idleTimeoutStr, 10) : undefined;
|
|
115
|
+
const maxIdle = maxIdleStr ? parseInt(maxIdleStr, 10) : undefined;
|
|
116
|
+
return {
|
|
117
|
+
host,
|
|
118
|
+
port,
|
|
119
|
+
user,
|
|
120
|
+
password,
|
|
121
|
+
database,
|
|
122
|
+
connectionLimit,
|
|
123
|
+
queueLimit,
|
|
124
|
+
connectTimeout,
|
|
125
|
+
idleTimeout,
|
|
126
|
+
maxIdle
|
|
127
|
+
};
|
|
128
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MySQL Database Access MCP Server
|
|
4
|
+
*
|
|
5
|
+
* This MCP server provides read-only access to MySQL databases.
|
|
6
|
+
* It allows:
|
|
7
|
+
* - Listing available databases
|
|
8
|
+
* - Listing tables in a database
|
|
9
|
+
* - Describing table schemas
|
|
10
|
+
* - Executing read-only SQL queries
|
|
11
|
+
*/
|
|
12
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
13
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
+
import { createConnectionPool, executeQuery, getConfigFromEnv } from './connection.js';
|
|
16
|
+
import { validateQuery } from './validators.js';
|
|
17
|
+
// Create MySQL connection pool
|
|
18
|
+
let pool;
|
|
19
|
+
try {
|
|
20
|
+
const config = getConfigFromEnv();
|
|
21
|
+
console.error('[Setup] MySQL configuration:', {
|
|
22
|
+
host: config.host,
|
|
23
|
+
port: config.port,
|
|
24
|
+
user: config.user,
|
|
25
|
+
password: config.password ? '********' : '(not provided)',
|
|
26
|
+
database: config.database || '(default not set)'
|
|
27
|
+
});
|
|
28
|
+
pool = createConnectionPool(config);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error('[Fatal] Failed to initialize MySQL connection:', error);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Create an MCP server with tools for MySQL database access
|
|
36
|
+
*/
|
|
37
|
+
const server = new Server({
|
|
38
|
+
name: "mysql-mcp-server",
|
|
39
|
+
version: "0.1.0",
|
|
40
|
+
}, {
|
|
41
|
+
capabilities: {
|
|
42
|
+
tools: {},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Handler that lists available tools for MySQL database access
|
|
47
|
+
*/
|
|
48
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
49
|
+
return {
|
|
50
|
+
tools: [
|
|
51
|
+
{
|
|
52
|
+
name: "list_databases",
|
|
53
|
+
description: "List all accessible databases on the MySQL server",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {},
|
|
57
|
+
required: []
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "list_tables",
|
|
62
|
+
description: "List all tables in a specified database",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
database: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Database name (optional, uses default if not specified)"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
required: []
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "describe_table",
|
|
76
|
+
description: "Show the schema for a specific table",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
database: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Database name (optional, uses default if not specified)"
|
|
83
|
+
},
|
|
84
|
+
table: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Table name"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
required: ["table"]
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "execute_query",
|
|
94
|
+
description: "Execute a read-only SQL query",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
query: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "SQL query (only SELECT, SHOW, DESCRIBE, and EXPLAIN statements are allowed)"
|
|
101
|
+
},
|
|
102
|
+
database: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "Database name (optional, uses default if not specified)"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
required: ["query"]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
/**
|
|
114
|
+
* Handler for MySQL database access tools
|
|
115
|
+
*/
|
|
116
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
117
|
+
try {
|
|
118
|
+
switch (request.params.name) {
|
|
119
|
+
case "list_databases": {
|
|
120
|
+
console.error('[Tool] Executing list_databases');
|
|
121
|
+
const { rows } = await executeQuery(pool, 'SHOW DATABASES');
|
|
122
|
+
return {
|
|
123
|
+
content: [{
|
|
124
|
+
type: "text",
|
|
125
|
+
text: JSON.stringify(rows, null, 2)
|
|
126
|
+
}]
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
case "list_tables": {
|
|
130
|
+
console.error('[Tool] Executing list_tables');
|
|
131
|
+
const database = request.params.arguments?.database;
|
|
132
|
+
const { rows } = await executeQuery(pool, 'SHOW FULL TABLES', [], database);
|
|
133
|
+
return {
|
|
134
|
+
content: [{
|
|
135
|
+
type: "text",
|
|
136
|
+
text: JSON.stringify(rows, null, 2)
|
|
137
|
+
}]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
case "describe_table": {
|
|
141
|
+
console.error('[Tool] Executing describe_table');
|
|
142
|
+
const database = request.params.arguments?.database;
|
|
143
|
+
const table = request.params.arguments?.table;
|
|
144
|
+
if (!table) {
|
|
145
|
+
throw new McpError(ErrorCode.InvalidParams, "Table name is required");
|
|
146
|
+
}
|
|
147
|
+
const { rows } = await executeQuery(pool, `DESCRIBE \`${table}\``, [], database);
|
|
148
|
+
return {
|
|
149
|
+
content: [{
|
|
150
|
+
type: "text",
|
|
151
|
+
text: JSON.stringify(rows, null, 2)
|
|
152
|
+
}]
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
case "execute_query": {
|
|
156
|
+
console.error('[Tool] Executing execute_query');
|
|
157
|
+
const query = request.params.arguments?.query;
|
|
158
|
+
const database = request.params.arguments?.database;
|
|
159
|
+
if (!query) {
|
|
160
|
+
throw new McpError(ErrorCode.InvalidParams, "Query is required");
|
|
161
|
+
}
|
|
162
|
+
// Validate that the query is read-only
|
|
163
|
+
validateQuery(query);
|
|
164
|
+
const { rows } = await executeQuery(pool, query, [], database);
|
|
165
|
+
return {
|
|
166
|
+
content: [{
|
|
167
|
+
type: "text",
|
|
168
|
+
text: JSON.stringify(rows, null, 2)
|
|
169
|
+
}]
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
default:
|
|
173
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
console.error('[Error] Tool execution failed:', error);
|
|
178
|
+
// Format error message for client
|
|
179
|
+
return {
|
|
180
|
+
content: [{
|
|
181
|
+
type: "text",
|
|
182
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
183
|
+
}],
|
|
184
|
+
isError: true
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
/**
|
|
189
|
+
* Start the server using stdio transport
|
|
190
|
+
*/
|
|
191
|
+
async function main() {
|
|
192
|
+
console.error('[Setup] Starting MySQL MCP server');
|
|
193
|
+
try {
|
|
194
|
+
const transport = new StdioServerTransport();
|
|
195
|
+
await server.connect(transport);
|
|
196
|
+
console.error('[Setup] MySQL MCP server running on stdio');
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
console.error('[Fatal] Failed to start server:', error);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Handle process termination
|
|
204
|
+
process.on('SIGINT', async () => {
|
|
205
|
+
console.error('[Shutdown] Closing MySQL connection pool');
|
|
206
|
+
await pool.end();
|
|
207
|
+
process.exit(0);
|
|
208
|
+
});
|
|
209
|
+
// Start the server
|
|
210
|
+
main().catch((error) => {
|
|
211
|
+
console.error('[Fatal] Unhandled error:', error);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
});
|
package/build/types.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL query validators for MySQL MCP server
|
|
3
|
+
* Ensures that only read-only queries are allowed
|
|
4
|
+
*/
|
|
5
|
+
// List of allowed SQL commands (read-only operations)
|
|
6
|
+
const ALLOWED_COMMANDS = [
|
|
7
|
+
'SELECT',
|
|
8
|
+
'SHOW',
|
|
9
|
+
'DESCRIBE',
|
|
10
|
+
'DESC',
|
|
11
|
+
'EXPLAIN',
|
|
12
|
+
];
|
|
13
|
+
// List of disallowed SQL commands (write operations)
|
|
14
|
+
const DISALLOWED_COMMANDS = [
|
|
15
|
+
'INSERT',
|
|
16
|
+
'UPDATE',
|
|
17
|
+
'DELETE',
|
|
18
|
+
'DROP',
|
|
19
|
+
'CREATE',
|
|
20
|
+
'ALTER',
|
|
21
|
+
'TRUNCATE',
|
|
22
|
+
'RENAME',
|
|
23
|
+
'REPLACE',
|
|
24
|
+
'GRANT',
|
|
25
|
+
'REVOKE',
|
|
26
|
+
'LOCK',
|
|
27
|
+
'UNLOCK',
|
|
28
|
+
'CALL',
|
|
29
|
+
'EXEC',
|
|
30
|
+
'EXECUTE',
|
|
31
|
+
'SET',
|
|
32
|
+
'START',
|
|
33
|
+
'BEGIN',
|
|
34
|
+
'COMMIT',
|
|
35
|
+
'ROLLBACK',
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Validates if a SQL query is read-only
|
|
39
|
+
* @param query SQL query to validate
|
|
40
|
+
* @returns true if the query is read-only, false otherwise
|
|
41
|
+
*/
|
|
42
|
+
export function isReadOnlyQuery(query) {
|
|
43
|
+
// Normalize query by removing comments and extra whitespace
|
|
44
|
+
const normalizedQuery = query
|
|
45
|
+
.replace(/--.*$/gm, '') // Remove single-line comments
|
|
46
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
|
|
47
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
48
|
+
.trim()
|
|
49
|
+
.toUpperCase();
|
|
50
|
+
// Check if query starts with an allowed command
|
|
51
|
+
const startsWithAllowed = ALLOWED_COMMANDS.some(cmd => normalizedQuery.startsWith(cmd + ' ') || normalizedQuery === cmd);
|
|
52
|
+
// Check if query contains any disallowed commands
|
|
53
|
+
const containsDisallowed = DISALLOWED_COMMANDS.some(cmd => {
|
|
54
|
+
const regex = new RegExp(`(^|\\s)${cmd}(\\s|$)`);
|
|
55
|
+
return regex.test(normalizedQuery);
|
|
56
|
+
});
|
|
57
|
+
// Check for multiple statements (;)
|
|
58
|
+
const hasMultipleStatements = normalizedQuery.includes(';') &&
|
|
59
|
+
!normalizedQuery.endsWith(';');
|
|
60
|
+
// Query is read-only if it starts with an allowed command,
|
|
61
|
+
// doesn't contain any disallowed commands, and doesn't have multiple statements
|
|
62
|
+
return startsWithAllowed && !containsDisallowed && !hasMultipleStatements;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Validates if a SQL query is safe to execute
|
|
66
|
+
* @param query SQL query to validate
|
|
67
|
+
* @throws Error if the query is not safe
|
|
68
|
+
*/
|
|
69
|
+
export function validateQuery(query) {
|
|
70
|
+
console.error('[Validator] Validating query:', query);
|
|
71
|
+
if (!query || typeof query !== 'string') {
|
|
72
|
+
throw new Error('Query must be a non-empty string');
|
|
73
|
+
}
|
|
74
|
+
if (!isReadOnlyQuery(query)) {
|
|
75
|
+
console.error('[Validator] Query rejected: not read-only');
|
|
76
|
+
throw new Error('Only read-only queries are allowed (SELECT, SHOW, DESCRIBE, EXPLAIN)');
|
|
77
|
+
}
|
|
78
|
+
console.error('[Validator] Query validated as read-only');
|
|
79
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cano721/mysql-mcp-server",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "An MCP server that provides read-only access to MySQL databases.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mysql-mcp-server": "build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"build"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
|
14
|
+
"prepare": "npm run build",
|
|
15
|
+
"watch": "tsc --watch",
|
|
16
|
+
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
|
|
17
|
+
"test:setup": "node test-setup.js",
|
|
18
|
+
"test:tools": "node test-tools.js",
|
|
19
|
+
"test": "npm run test:setup && npm run test:tools"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"mysql",
|
|
24
|
+
"database",
|
|
25
|
+
"model-context-protocol",
|
|
26
|
+
"ai",
|
|
27
|
+
"llm"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/cano721/mysql-mcp-server.git"
|
|
32
|
+
},
|
|
33
|
+
"author": "cano721",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@modelcontextprotocol/sdk": "0.6.0",
|
|
37
|
+
"mysql2": "^3.12.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20.11.24",
|
|
41
|
+
"dotenv": "^16.4.7",
|
|
42
|
+
"typescript": "^5.3.3"
|
|
43
|
+
}
|
|
44
|
+
}
|