@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 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
+ }
@@ -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
+ }