@cemalturkcann/mariadb-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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/config.example.json +27 -0
- package/package.json +62 -0
- package/src/config.js +92 -0
- package/src/db.js +151 -0
- package/src/index.js +191 -0
- package/src/sqlGuard.js +103 -0
- package/src/tools.js +135 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Cemal Turkcan
|
|
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,108 @@
|
|
|
1
|
+
# mariadb-mcp-server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for MariaDB/MySQL databases. Provides AI assistants with safe, controlled database access through per-connection read/write permissions.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-connection** — manage multiple MariaDB/MySQL databases from a single server
|
|
8
|
+
- **Per-connection access control** — `read` and `write` flags per connection
|
|
9
|
+
- **Optional limits** — `statement_timeout_ms`, `default_row_limit`, `max_row_limit` (all optional, unlimited by default)
|
|
10
|
+
- **SQL guard** — validates queries to prevent accidental writes through read tools
|
|
11
|
+
- **Transaction support** — atomic multi-query execution on writable connections
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g mariadb-mcp-server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or use directly with npx:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx mariadb-mcp-server
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
Create a `config.json` next to the package (or set `DB_MCP_CONFIG_PATH` env var):
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"connections": {
|
|
32
|
+
"local": {
|
|
33
|
+
"host": "localhost",
|
|
34
|
+
"port": 3306,
|
|
35
|
+
"user": "root",
|
|
36
|
+
"password": "",
|
|
37
|
+
"description": "Local MariaDB",
|
|
38
|
+
"read": true,
|
|
39
|
+
"write": true
|
|
40
|
+
},
|
|
41
|
+
"production": {
|
|
42
|
+
"host": "db.example.com",
|
|
43
|
+
"port": 3306,
|
|
44
|
+
"user": "readonly_user",
|
|
45
|
+
"password": "secret",
|
|
46
|
+
"database": "mydb",
|
|
47
|
+
"description": "Production (read-only)",
|
|
48
|
+
"read": true,
|
|
49
|
+
"write": false,
|
|
50
|
+
"statement_timeout_ms": 5000,
|
|
51
|
+
"default_row_limit": 50,
|
|
52
|
+
"max_row_limit": 500
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Connection options
|
|
59
|
+
|
|
60
|
+
| Field | Type | Default | Description |
|
|
61
|
+
|-------|------|---------|-------------|
|
|
62
|
+
| `host` | string | `localhost` | Database host |
|
|
63
|
+
| `port` | number | `3306` | Database port |
|
|
64
|
+
| `user` | string | `root` | Database user |
|
|
65
|
+
| `password` | string | `""` | Database password |
|
|
66
|
+
| `database` | string | `""` | Default database |
|
|
67
|
+
| `description` | string | `""` | Human-readable label |
|
|
68
|
+
| `read` | boolean | `true` | Allow read queries |
|
|
69
|
+
| `write` | boolean | `false` | Allow write queries |
|
|
70
|
+
| `ssl` | boolean/object | `false` | SSL configuration |
|
|
71
|
+
| `statement_timeout_ms` | number | *none* | Connection timeout (0 or omit = unlimited) |
|
|
72
|
+
| `default_row_limit` | number | *none* | Default LIMIT for SELECT (0 or omit = unlimited) |
|
|
73
|
+
| `max_row_limit` | number | *none* | Max allowed LIMIT (0 or omit = unlimited) |
|
|
74
|
+
|
|
75
|
+
## MCP Tools
|
|
76
|
+
|
|
77
|
+
| Tool | Description | Requires |
|
|
78
|
+
|------|-------------|----------|
|
|
79
|
+
| `list_connections` | List all configured connections | — |
|
|
80
|
+
| `list_databases` | Show databases on a connection | `read` |
|
|
81
|
+
| `list_tables` | Show tables (optionally in a specific database) | `read` |
|
|
82
|
+
| `describe_table` | Show column definitions | `read` |
|
|
83
|
+
| `execute_select` | Run SELECT/SHOW/DESCRIBE/EXPLAIN queries | `read` |
|
|
84
|
+
| `execute_write` | Run INSERT/UPDATE/DELETE/DDL queries | `write` |
|
|
85
|
+
| `execute_transaction` | Run multiple write queries atomically | `write` |
|
|
86
|
+
| `suggest_query` | Suggest a query for manual review | — |
|
|
87
|
+
|
|
88
|
+
## Claude Desktop / MCP Client Setup
|
|
89
|
+
|
|
90
|
+
Add to your MCP client config:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"mariadb": {
|
|
96
|
+
"command": "npx",
|
|
97
|
+
"args": ["-y", "mariadb-mcp-server"],
|
|
98
|
+
"env": {
|
|
99
|
+
"DB_MCP_CONFIG_PATH": "/path/to/your/config.json"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"connections": {
|
|
3
|
+
"local": {
|
|
4
|
+
"host": "localhost",
|
|
5
|
+
"port": 3306,
|
|
6
|
+
"user": "root",
|
|
7
|
+
"password": "",
|
|
8
|
+
"database": "",
|
|
9
|
+
"description": "Local MariaDB",
|
|
10
|
+
"read": true,
|
|
11
|
+
"write": true
|
|
12
|
+
},
|
|
13
|
+
"production": {
|
|
14
|
+
"host": "db.example.com",
|
|
15
|
+
"port": 3306,
|
|
16
|
+
"user": "readonly_user",
|
|
17
|
+
"password": "secret",
|
|
18
|
+
"database": "mydb",
|
|
19
|
+
"description": "Production (read-only)",
|
|
20
|
+
"read": true,
|
|
21
|
+
"write": false,
|
|
22
|
+
"statement_timeout_ms": 5000,
|
|
23
|
+
"default_row_limit": 50,
|
|
24
|
+
"max_row_limit": 500
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cemalturkcann/mariadb-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for MariaDB/MySQL with per-connection read/write access control",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mariadb-mcp-server": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"config.example.json",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node src/index.js",
|
|
17
|
+
"check": "node --check src/index.js && node --check src/config.js && node --check src/db.js && node --check src/sqlGuard.js && node --check src/tools.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"mariadb",
|
|
22
|
+
"mysql",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"database",
|
|
25
|
+
"ai",
|
|
26
|
+
"llm",
|
|
27
|
+
"claude"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/cemalturkcan/mariadb-mcp-server.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/cemalturkcan/mariadb-mcp-server#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/cemalturkcan/mariadb-mcp-server/issues"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
43
|
+
"mariadb": "^3.3.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"semantic-release": "^24.0.0"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"release": {
|
|
52
|
+
"branches": [
|
|
53
|
+
"main"
|
|
54
|
+
],
|
|
55
|
+
"plugins": [
|
|
56
|
+
"@semantic-release/commit-analyzer",
|
|
57
|
+
"@semantic-release/release-notes-generator",
|
|
58
|
+
"@semantic-release/npm",
|
|
59
|
+
"@semantic-release/github"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
function toPositiveInt(value, fallback) {
|
|
8
|
+
const n = Number(value);
|
|
9
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
10
|
+
return Math.floor(n);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toNonNegativeInt(value) {
|
|
14
|
+
if (value == null) return 0;
|
|
15
|
+
const n = Number(value);
|
|
16
|
+
if (!Number.isFinite(n) || n < 0) return 0;
|
|
17
|
+
return Math.floor(n);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeConnection(connection) {
|
|
21
|
+
return {
|
|
22
|
+
host: connection.host || "localhost",
|
|
23
|
+
port: toPositiveInt(connection.port, 3306),
|
|
24
|
+
user: connection.user || "root",
|
|
25
|
+
password: connection.password || "",
|
|
26
|
+
database: connection.database || "",
|
|
27
|
+
description: connection.description || "",
|
|
28
|
+
read: connection.read !== false,
|
|
29
|
+
write: connection.write === true,
|
|
30
|
+
statement_timeout_ms: toNonNegativeInt(connection.statement_timeout_ms),
|
|
31
|
+
default_row_limit: toNonNegativeInt(connection.default_row_limit),
|
|
32
|
+
max_row_limit: toNonNegativeInt(connection.max_row_limit),
|
|
33
|
+
ssl: connection.ssl || false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function loadConfig() {
|
|
38
|
+
const candidates = [
|
|
39
|
+
process.env.DB_MCP_CONFIG_PATH,
|
|
40
|
+
join(__dirname, "../config.json"),
|
|
41
|
+
join(process.cwd(), "config.json"),
|
|
42
|
+
].filter(Boolean);
|
|
43
|
+
|
|
44
|
+
let rawConfig;
|
|
45
|
+
for (const configPath of candidates) {
|
|
46
|
+
if (existsSync(configPath)) {
|
|
47
|
+
rawConfig = JSON.parse(readFileSync(configPath, "utf8"));
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!rawConfig) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"config.json bulunamadi. DB_MCP_CONFIG_PATH ayarlayin veya proje kokune config.json koyun."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const raw = rawConfig.connections || rawConfig.databases;
|
|
59
|
+
if (!raw || typeof raw !== "object") {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"config.json icinde 'connections' (veya eski format 'databases') nesnesi olmalidir."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const entries = Object.entries(raw);
|
|
66
|
+
if (entries.length === 0) {
|
|
67
|
+
throw new Error("En az bir baglanti tanimlamalisiniz.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const connections = {};
|
|
71
|
+
for (const [name, connection] of entries) {
|
|
72
|
+
if (!connection || typeof connection !== "object") {
|
|
73
|
+
throw new Error(`'${name}' baglantisi gecersiz.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const normalized = normalizeConnection(connection);
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
normalized.default_row_limit > 0 &&
|
|
80
|
+
normalized.max_row_limit > 0 &&
|
|
81
|
+
normalized.default_row_limit > normalized.max_row_limit
|
|
82
|
+
) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`'${name}' icin default_row_limit, max_row_limit degerinden buyuk olamaz.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
connections[name] = normalized;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { connections };
|
|
92
|
+
}
|
package/src/db.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import mariadb from "mariadb";
|
|
2
|
+
|
|
3
|
+
export class DbManager {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
this.pools = new Map();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
getConnectionNames() {
|
|
10
|
+
return Object.keys(this.config.connections);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getReadableConnections() {
|
|
14
|
+
return this.getConnectionNames().filter(
|
|
15
|
+
(name) => this.config.connections[name].read
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getWritableConnections() {
|
|
20
|
+
return this.getConnectionNames().filter(
|
|
21
|
+
(name) => this.config.connections[name].write
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getConnectionConfig(name) {
|
|
26
|
+
const cfg = this.config.connections[name];
|
|
27
|
+
if (!cfg) {
|
|
28
|
+
const available = this.getConnectionNames().join(", ");
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Baglanti bulunamadi: '${name}'. Mevcut baglantilar: ${available}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return cfg;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getPool(name, databaseOverride = null) {
|
|
37
|
+
const c = this.getConnectionConfig(name);
|
|
38
|
+
const database = databaseOverride || c.database;
|
|
39
|
+
const poolKey = `${name}::${database}`;
|
|
40
|
+
|
|
41
|
+
if (!this.pools.has(poolKey)) {
|
|
42
|
+
const poolOptions = {
|
|
43
|
+
host: c.host,
|
|
44
|
+
port: c.port,
|
|
45
|
+
user: c.user,
|
|
46
|
+
password: c.password,
|
|
47
|
+
database: database || undefined,
|
|
48
|
+
connectionLimit: 5,
|
|
49
|
+
insertIdAsNumber: true,
|
|
50
|
+
bigIntAsNumber: true,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (c.statement_timeout_ms > 0) {
|
|
54
|
+
poolOptions.connectTimeout = c.statement_timeout_ms;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (c.ssl) {
|
|
58
|
+
poolOptions.ssl =
|
|
59
|
+
typeof c.ssl === "object" ? c.ssl : { rejectUnauthorized: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.pools.set(poolKey, mariadb.createPool(poolOptions));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return this.pools.get(poolKey);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
assertReadable(connectionName) {
|
|
69
|
+
const cfg = this.getConnectionConfig(connectionName);
|
|
70
|
+
if (!cfg.read) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`'${connectionName}' baglantisi read (okuma) izni vermiyor.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
assertWritable(connectionName) {
|
|
78
|
+
const cfg = this.getConnectionConfig(connectionName);
|
|
79
|
+
if (!cfg.write) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`'${connectionName}' baglantisi write (yazma) izni vermiyor.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async runReadOnly(connectionName, sql, options = {}) {
|
|
87
|
+
this.assertReadable(connectionName);
|
|
88
|
+
const database = options.database || null;
|
|
89
|
+
const conn = await this.getPool(connectionName, database).getConnection();
|
|
90
|
+
try {
|
|
91
|
+
return await conn.query(sql);
|
|
92
|
+
} finally {
|
|
93
|
+
conn.release();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async runWrite(connectionName, sql, options = {}) {
|
|
98
|
+
this.assertWritable(connectionName);
|
|
99
|
+
const database = options.database || null;
|
|
100
|
+
const conn = await this.getPool(connectionName, database).getConnection();
|
|
101
|
+
try {
|
|
102
|
+
const result = await conn.query(sql);
|
|
103
|
+
return {
|
|
104
|
+
affectedRows: result.affectedRows,
|
|
105
|
+
insertId: result.insertId,
|
|
106
|
+
warningStatus: result.warningStatus,
|
|
107
|
+
};
|
|
108
|
+
} finally {
|
|
109
|
+
conn.release();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async runTransaction(connectionName, queries, options = {}) {
|
|
114
|
+
this.assertWritable(connectionName);
|
|
115
|
+
const database = options.database || null;
|
|
116
|
+
const conn = await this.getPool(connectionName, database).getConnection();
|
|
117
|
+
try {
|
|
118
|
+
await conn.beginTransaction();
|
|
119
|
+
const results = [];
|
|
120
|
+
for (const q of queries) {
|
|
121
|
+
const r = await conn.query(q);
|
|
122
|
+
results.push({
|
|
123
|
+
query: q.substring(0, 100),
|
|
124
|
+
affectedRows: r.affectedRows,
|
|
125
|
+
insertId: r.insertId,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
await conn.commit();
|
|
129
|
+
return { committed: true, results };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
try {
|
|
132
|
+
await conn.rollback();
|
|
133
|
+
} catch {
|
|
134
|
+
/* asil hatayi koruyoruz */
|
|
135
|
+
}
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Transaction basarisiz, rollback yapildi: ${error.message}`
|
|
138
|
+
);
|
|
139
|
+
} finally {
|
|
140
|
+
conn.release();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async closeAll() {
|
|
145
|
+
const tasks = [];
|
|
146
|
+
for (const pool of this.pools.values()) {
|
|
147
|
+
tasks.push(pool.end());
|
|
148
|
+
}
|
|
149
|
+
await Promise.all(tasks);
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
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
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { loadConfig } from "./config.js";
|
|
10
|
+
import { DbManager } from "./db.js";
|
|
11
|
+
import {
|
|
12
|
+
validateReadQuery,
|
|
13
|
+
validateWriteQuery,
|
|
14
|
+
resolveRowLimit,
|
|
15
|
+
buildLimitedQuery,
|
|
16
|
+
} from "./sqlGuard.js";
|
|
17
|
+
import { buildToolDefinitions, fail, ok } from "./tools.js";
|
|
18
|
+
|
|
19
|
+
const config = loadConfig();
|
|
20
|
+
const db = new DbManager(config);
|
|
21
|
+
|
|
22
|
+
const allConnections = db.getConnectionNames();
|
|
23
|
+
const readableConnections = db.getReadableConnections();
|
|
24
|
+
const writableConnections = db.getWritableConnections();
|
|
25
|
+
|
|
26
|
+
const server = new Server(
|
|
27
|
+
{ name: "mariadb-mcp", version: "2.0.0" },
|
|
28
|
+
{ capabilities: { tools: {} } },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
32
|
+
tools: buildToolDefinitions(
|
|
33
|
+
readableConnections,
|
|
34
|
+
writableConnections,
|
|
35
|
+
allConnections,
|
|
36
|
+
),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
40
|
+
const { name, arguments: args } = request.params;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
switch (name) {
|
|
44
|
+
case "list_connections": {
|
|
45
|
+
const list = allConnections.map((connectionName) => {
|
|
46
|
+
const c = db.getConnectionConfig(connectionName);
|
|
47
|
+
return {
|
|
48
|
+
name: connectionName,
|
|
49
|
+
description: c.description,
|
|
50
|
+
host: c.host,
|
|
51
|
+
port: c.port,
|
|
52
|
+
database: c.database,
|
|
53
|
+
read: c.read,
|
|
54
|
+
write: c.write,
|
|
55
|
+
...(c.statement_timeout_ms > 0 && {
|
|
56
|
+
statement_timeout_ms: c.statement_timeout_ms,
|
|
57
|
+
}),
|
|
58
|
+
...(c.default_row_limit > 0 && {
|
|
59
|
+
default_row_limit: c.default_row_limit,
|
|
60
|
+
}),
|
|
61
|
+
...(c.max_row_limit > 0 && { max_row_limit: c.max_row_limit }),
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
return ok(list);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case "list_databases": {
|
|
68
|
+
const connection = args?.connection;
|
|
69
|
+
if (!connection) return fail("'connection' alani zorunludur.");
|
|
70
|
+
const rows = await db.runReadOnly(connection, "SHOW DATABASES");
|
|
71
|
+
return ok(rows);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
case "list_tables": {
|
|
75
|
+
const { connection, database } = args || {};
|
|
76
|
+
if (!connection) return fail("'connection' alani zorunludur.");
|
|
77
|
+
const sql = database
|
|
78
|
+
? `SHOW TABLES FROM \`${database}\``
|
|
79
|
+
: "SHOW TABLES";
|
|
80
|
+
const rows = await db.runReadOnly(connection, sql);
|
|
81
|
+
return ok(rows);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case "describe_table": {
|
|
85
|
+
const { connection, table, database } = args || {};
|
|
86
|
+
if (!connection) return fail("'connection' alani zorunludur.");
|
|
87
|
+
if (!table) return fail("'table' alani zorunludur.");
|
|
88
|
+
const path = database ? `\`${database}\`.\`${table}\`` : `\`${table}\``;
|
|
89
|
+
const rows = await db.runReadOnly(connection, `DESCRIBE ${path}`);
|
|
90
|
+
return ok(rows);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case "execute_select": {
|
|
94
|
+
const connection = args?.connection;
|
|
95
|
+
const query = args?.query;
|
|
96
|
+
const database = args?.database;
|
|
97
|
+
const requestedRowLimit = args?.row_limit;
|
|
98
|
+
|
|
99
|
+
if (!connection) return fail("'connection' alani zorunludur.");
|
|
100
|
+
if (!query) return fail("'query' alani zorunludur.");
|
|
101
|
+
|
|
102
|
+
const connectionConfig = db.getConnectionConfig(connection);
|
|
103
|
+
const validatedQuery = validateReadQuery(query);
|
|
104
|
+
|
|
105
|
+
const upper = validatedQuery.trim().toUpperCase();
|
|
106
|
+
let finalQuery = validatedQuery;
|
|
107
|
+
let appliedRowLimit = null;
|
|
108
|
+
|
|
109
|
+
if (upper.startsWith("SELECT") || upper.startsWith("WITH")) {
|
|
110
|
+
appliedRowLimit = resolveRowLimit(
|
|
111
|
+
requestedRowLimit,
|
|
112
|
+
connectionConfig,
|
|
113
|
+
);
|
|
114
|
+
if (appliedRowLimit !== null) {
|
|
115
|
+
finalQuery = buildLimitedQuery(validatedQuery, appliedRowLimit);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const rows = await db.runReadOnly(connection, finalQuery, { database });
|
|
120
|
+
|
|
121
|
+
const result = {
|
|
122
|
+
row_count: Array.isArray(rows) ? rows.length : 0,
|
|
123
|
+
rows,
|
|
124
|
+
};
|
|
125
|
+
if (appliedRowLimit !== null) {
|
|
126
|
+
result.row_limit = appliedRowLimit;
|
|
127
|
+
}
|
|
128
|
+
return ok(result);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case "execute_write": {
|
|
132
|
+
const { connection, query } = args || {};
|
|
133
|
+
if (!connection) return fail("'connection' alani zorunludur.");
|
|
134
|
+
if (!query) return fail("'query' alani zorunludur.");
|
|
135
|
+
|
|
136
|
+
const validatedQuery = validateWriteQuery(query);
|
|
137
|
+
const meta = await db.runWrite(connection, validatedQuery);
|
|
138
|
+
return ok({ success: true, meta });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case "execute_transaction": {
|
|
142
|
+
const { connection, queries } = args || {};
|
|
143
|
+
if (!connection) return fail("'connection' alani zorunludur.");
|
|
144
|
+
if (!Array.isArray(queries) || queries.length === 0) {
|
|
145
|
+
return fail("'queries' en az bir sorgu iceren bir dizi olmalidir.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const q of queries) {
|
|
149
|
+
validateWriteQuery(q);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const result = await db.runTransaction(connection, queries);
|
|
153
|
+
return ok(result);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case "suggest_query": {
|
|
157
|
+
const { connection, query, reason } = args || {};
|
|
158
|
+
return ok({
|
|
159
|
+
message: "MANUEL CALISTIRMA GEREKLI",
|
|
160
|
+
connection,
|
|
161
|
+
reason: reason || "Yazma islemi",
|
|
162
|
+
query,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
default:
|
|
167
|
+
return fail(`Bilinmeyen arac: ${name}`);
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
171
|
+
return fail(`Hata: ${message}`);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
async function main() {
|
|
176
|
+
const transport = new StdioServerTransport();
|
|
177
|
+
await server.connect(transport);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
main().catch((error) => {
|
|
181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
182
|
+
console.error(`Sunucu baslatilamadi: ${message}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
187
|
+
process.on(signal, async () => {
|
|
188
|
+
await db.closeAll();
|
|
189
|
+
process.exit(0);
|
|
190
|
+
});
|
|
191
|
+
}
|
package/src/sqlGuard.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const READ_PREFIXES = ["SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN", "WITH"];
|
|
2
|
+
|
|
3
|
+
const WRITE_PREFIXES = [
|
|
4
|
+
"INSERT",
|
|
5
|
+
"UPDATE",
|
|
6
|
+
"DELETE",
|
|
7
|
+
"REPLACE",
|
|
8
|
+
"CREATE",
|
|
9
|
+
"ALTER",
|
|
10
|
+
"DROP",
|
|
11
|
+
"TRUNCATE",
|
|
12
|
+
"RENAME",
|
|
13
|
+
"SET",
|
|
14
|
+
"START TRANSACTION",
|
|
15
|
+
"COMMIT",
|
|
16
|
+
"ROLLBACK",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function isReadQuery(sql) {
|
|
20
|
+
const t = sql.trim().toUpperCase();
|
|
21
|
+
return READ_PREFIXES.some((p) => t.startsWith(p));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isWriteQuery(sql) {
|
|
25
|
+
const t = sql.trim().toUpperCase();
|
|
26
|
+
return WRITE_PREFIXES.some((p) => t.startsWith(p));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validateReadQuery(rawQuery) {
|
|
30
|
+
if (typeof rawQuery !== "string") {
|
|
31
|
+
throw new Error("Sorgu metin (string) olmalidir.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const query = rawQuery.trim();
|
|
35
|
+
if (!query) {
|
|
36
|
+
throw new Error("Sorgu bos olamaz.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!isReadQuery(query)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Yalnizca SELECT/SHOW/DESCRIBE/EXPLAIN sorgularina izin verilir."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return query;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validateWriteQuery(rawQuery) {
|
|
49
|
+
if (typeof rawQuery !== "string") {
|
|
50
|
+
throw new Error("Sorgu metin (string) olmalidir.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const query = rawQuery.trim();
|
|
54
|
+
if (!query) {
|
|
55
|
+
throw new Error("Sorgu bos olamaz.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (isReadQuery(query)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Read-only sorgular (SELECT/SHOW/DESCRIBE/EXPLAIN) execute_write'da kullanilamaz. execute_select kullanin."
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!isWriteQuery(query)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Sorgu gecerli bir yazma islemi olarak taninamadi. Izin verilen: ${WRITE_PREFIXES.join(", ")}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return query;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolves the effective row limit for a query.
|
|
75
|
+
* Returns null when no limit should be applied.
|
|
76
|
+
*
|
|
77
|
+
* - Config'de default_row_limit / max_row_limit yoksa (0) → sinirsiz
|
|
78
|
+
* - Kullanici row_limit verdiyse ve max_row_limit varsa → min(request, max) uygulanir
|
|
79
|
+
* - Kullanici row_limit vermediyse ama default_row_limit varsa → default uygulanir
|
|
80
|
+
*/
|
|
81
|
+
export function resolveRowLimit(requestedRowLimit, connectionConfig) {
|
|
82
|
+
const defaultLimit = connectionConfig.default_row_limit || 0;
|
|
83
|
+
const maxLimit = connectionConfig.max_row_limit || 0;
|
|
84
|
+
|
|
85
|
+
if (requestedRowLimit != null) {
|
|
86
|
+
const n = Number(requestedRowLimit);
|
|
87
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
88
|
+
throw new Error("row_limit pozitif bir sayi olmalidir.");
|
|
89
|
+
}
|
|
90
|
+
const chosen = Math.floor(n);
|
|
91
|
+
return maxLimit > 0 ? Math.min(chosen, maxLimit) : chosen;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (defaultLimit > 0) {
|
|
95
|
+
return maxLimit > 0 ? Math.min(defaultLimit, maxLimit) : defaultLimit;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildLimitedQuery(query, rowLimit) {
|
|
102
|
+
return `SELECT * FROM (${query}) AS mcp_query LIMIT ${rowLimit}`;
|
|
103
|
+
}
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export function buildToolDefinitions(
|
|
2
|
+
readableConnections,
|
|
3
|
+
writableConnections,
|
|
4
|
+
allConnections
|
|
5
|
+
) {
|
|
6
|
+
const tools = [
|
|
7
|
+
{
|
|
8
|
+
name: "list_connections",
|
|
9
|
+
description: "Tanimli MariaDB baglantilarini listeler.",
|
|
10
|
+
inputSchema: { type: "object", properties: {} },
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: "list_databases",
|
|
14
|
+
description: "Secilen baglantidaki veritabanlarini listeler.",
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
connection: { type: "string", enum: readableConnections },
|
|
19
|
+
},
|
|
20
|
+
required: ["connection"],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "list_tables",
|
|
25
|
+
description: "Secilen baglantida tablolari listeler.",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
connection: { type: "string", enum: readableConnections },
|
|
30
|
+
database: { type: "string" },
|
|
31
|
+
},
|
|
32
|
+
required: ["connection"],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "describe_table",
|
|
37
|
+
description: "Bir tablonun kolon bilgilerini dondurur.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
connection: { type: "string", enum: readableConnections },
|
|
42
|
+
table: { type: "string" },
|
|
43
|
+
database: { type: "string" },
|
|
44
|
+
},
|
|
45
|
+
required: ["connection", "table"],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "execute_select",
|
|
50
|
+
description:
|
|
51
|
+
"Salt-okunur SELECT/SHOW/DESCRIBE/EXPLAIN sorgusu calistirir. Satir limiti uygulanir.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
connection: { type: "string", enum: readableConnections },
|
|
56
|
+
query: { type: "string" },
|
|
57
|
+
database: { type: "string" },
|
|
58
|
+
row_limit: { type: "number" },
|
|
59
|
+
},
|
|
60
|
+
required: ["connection", "query"],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
if (writableConnections.length > 0) {
|
|
66
|
+
tools.push(
|
|
67
|
+
{
|
|
68
|
+
name: "execute_write",
|
|
69
|
+
description: `INSERT/UPDATE/DELETE/CREATE/ALTER/DROP vb. yazma sorgusu calistirir. Baglantilar: ${writableConnections.join(", ")}`,
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
connection: { type: "string", enum: writableConnections },
|
|
74
|
+
query: { type: "string" },
|
|
75
|
+
},
|
|
76
|
+
required: ["connection", "query"],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "execute_transaction",
|
|
81
|
+
description: `Birden fazla yazma sorgusunu transaction icinde calistirir. Baglantilar: ${writableConnections.join(", ")}`,
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
connection: { type: "string", enum: writableConnections },
|
|
86
|
+
queries: {
|
|
87
|
+
type: "array",
|
|
88
|
+
items: { type: "string" },
|
|
89
|
+
minItems: 1,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
required: ["connection", "queries"],
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
tools.push({
|
|
99
|
+
name: "suggest_query",
|
|
100
|
+
description: "Manuel calistirilmasi gereken bir sorgu onerir.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
connection: { type: "string" },
|
|
105
|
+
query: { type: "string" },
|
|
106
|
+
reason: { type: "string" },
|
|
107
|
+
},
|
|
108
|
+
required: ["connection", "query"],
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return tools;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function ok(payload) {
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: JSON.stringify(
|
|
121
|
+
payload,
|
|
122
|
+
(key, value) => (typeof value === "bigint" ? Number(value) : value),
|
|
123
|
+
2
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function fail(message) {
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: message }],
|
|
133
|
+
isError: true,
|
|
134
|
+
};
|
|
135
|
+
}
|