@cemalturkcann/mariadb-mcp-server 1.1.1 → 1.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/README.md +5 -9
- package/package.json +1 -1
- package/src/config.js +4 -4
- package/src/db.js +9 -9
- package/src/index.js +15 -15
- package/src/sqlGuard.js +11 -11
- package/src/tools.js +8 -8
package/README.md
CHANGED
|
@@ -96,6 +96,8 @@ Example config:
|
|
|
96
96
|
|
|
97
97
|
## MCP Client Setup
|
|
98
98
|
|
|
99
|
+
On first run, a default config is created automatically at `~/.config/mariadb-mcp/config.json` (Linux/macOS) or `%APPDATA%\mariadb-mcp\config.json` (Windows). Edit it to add your connections.
|
|
100
|
+
|
|
99
101
|
### Claude Code
|
|
100
102
|
|
|
101
103
|
Add to `~/.claude.json`:
|
|
@@ -105,10 +107,7 @@ Add to `~/.claude.json`:
|
|
|
105
107
|
"mcpServers": {
|
|
106
108
|
"mariadb": {
|
|
107
109
|
"command": "npx",
|
|
108
|
-
"args": ["-y", "@cemalturkcann/mariadb-mcp-server"]
|
|
109
|
-
"env": {
|
|
110
|
-
"DB_MCP_CONFIG_PATH": "/path/to/config.json"
|
|
111
|
-
}
|
|
110
|
+
"args": ["-y", "@cemalturkcann/mariadb-mcp-server"]
|
|
112
111
|
}
|
|
113
112
|
}
|
|
114
113
|
}
|
|
@@ -123,10 +122,7 @@ Add to your `opencode.json`:
|
|
|
123
122
|
"mcp": {
|
|
124
123
|
"mariadb": {
|
|
125
124
|
"type": "local",
|
|
126
|
-
"command": ["npx", "-y", "@cemalturkcann/mariadb-mcp-server"]
|
|
127
|
-
"environment": {
|
|
128
|
-
"DB_MCP_CONFIG_PATH": "/path/to/config.json"
|
|
129
|
-
}
|
|
125
|
+
"command": ["npx", "-y", "@cemalturkcann/mariadb-mcp-server"]
|
|
130
126
|
}
|
|
131
127
|
}
|
|
132
128
|
}
|
|
@@ -134,7 +130,7 @@ Add to your `opencode.json`:
|
|
|
134
130
|
|
|
135
131
|
### Other MCP Clients
|
|
136
132
|
|
|
137
|
-
Any MCP-compatible client can use this server. The binary name is `mariadb-mcp-server` and it communicates over stdio.
|
|
133
|
+
Any MCP-compatible client can use this server. The binary name is `mariadb-mcp-server` and it communicates over stdio. To use a custom config path, set `DB_MCP_CONFIG_PATH`.
|
|
138
134
|
|
|
139
135
|
## License
|
|
140
136
|
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -99,19 +99,19 @@ export function loadConfig() {
|
|
|
99
99
|
const raw = rawConfig.connections || rawConfig.databases;
|
|
100
100
|
if (!raw || typeof raw !== "object") {
|
|
101
101
|
throw new Error(
|
|
102
|
-
"config.json
|
|
102
|
+
"config.json must contain a 'connections' (or legacy 'databases') object.",
|
|
103
103
|
);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const entries = Object.entries(raw);
|
|
107
107
|
if (entries.length === 0) {
|
|
108
|
-
throw new Error("
|
|
108
|
+
throw new Error("You must define at least one connection.");
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
const connections = {};
|
|
112
112
|
for (const [name, connection] of entries) {
|
|
113
113
|
if (!connection || typeof connection !== "object") {
|
|
114
|
-
throw new Error(`'${name}'
|
|
114
|
+
throw new Error(`'${name}' connection is invalid.`);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
const normalized = normalizeConnection(connection);
|
|
@@ -122,7 +122,7 @@ export function loadConfig() {
|
|
|
122
122
|
normalized.default_row_limit > normalized.max_row_limit
|
|
123
123
|
) {
|
|
124
124
|
throw new Error(
|
|
125
|
-
`'${name}'
|
|
125
|
+
`default_row_limit for '${name}' cannot exceed max_row_limit.`,
|
|
126
126
|
);
|
|
127
127
|
}
|
|
128
128
|
|
package/src/db.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createPool } from "mariadb";
|
|
2
2
|
|
|
3
3
|
export class DbManager {
|
|
4
4
|
constructor(config) {
|
|
@@ -12,13 +12,13 @@ export class DbManager {
|
|
|
12
12
|
|
|
13
13
|
getReadableConnections() {
|
|
14
14
|
return this.getConnectionNames().filter(
|
|
15
|
-
(name) => this.config.connections[name].read
|
|
15
|
+
(name) => this.config.connections[name].read,
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
getWritableConnections() {
|
|
20
20
|
return this.getConnectionNames().filter(
|
|
21
|
-
(name) => this.config.connections[name].write
|
|
21
|
+
(name) => this.config.connections[name].write,
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -27,7 +27,7 @@ export class DbManager {
|
|
|
27
27
|
if (!cfg) {
|
|
28
28
|
const available = this.getConnectionNames().join(", ");
|
|
29
29
|
throw new Error(
|
|
30
|
-
`
|
|
30
|
+
`Connection not found: '${name}'. Available connections: ${available}`,
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
return cfg;
|
|
@@ -59,7 +59,7 @@ export class DbManager {
|
|
|
59
59
|
typeof c.ssl === "object" ? c.ssl : { rejectUnauthorized: false };
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
this.pools.set(poolKey,
|
|
62
|
+
this.pools.set(poolKey, createPool(poolOptions));
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
return this.pools.get(poolKey);
|
|
@@ -69,7 +69,7 @@ export class DbManager {
|
|
|
69
69
|
const cfg = this.getConnectionConfig(connectionName);
|
|
70
70
|
if (!cfg.read) {
|
|
71
71
|
throw new Error(
|
|
72
|
-
`'${connectionName}'
|
|
72
|
+
`'${connectionName}' connection does not allow read access.`,
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -78,7 +78,7 @@ export class DbManager {
|
|
|
78
78
|
const cfg = this.getConnectionConfig(connectionName);
|
|
79
79
|
if (!cfg.write) {
|
|
80
80
|
throw new Error(
|
|
81
|
-
`'${connectionName}'
|
|
81
|
+
`'${connectionName}' connection does not allow write access.`,
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
}
|
|
@@ -131,10 +131,10 @@ export class DbManager {
|
|
|
131
131
|
try {
|
|
132
132
|
await conn.rollback();
|
|
133
133
|
} catch {
|
|
134
|
-
/*
|
|
134
|
+
/* preserve the original error */
|
|
135
135
|
}
|
|
136
136
|
throw new Error(
|
|
137
|
-
`Transaction
|
|
137
|
+
`Transaction failed, rolled back: ${error.message}`,
|
|
138
138
|
);
|
|
139
139
|
} finally {
|
|
140
140
|
conn.release();
|
package/src/index.js
CHANGED
|
@@ -66,14 +66,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
66
66
|
|
|
67
67
|
case "list_databases": {
|
|
68
68
|
const connection = args?.connection;
|
|
69
|
-
if (!connection) return fail("'connection'
|
|
69
|
+
if (!connection) return fail("'connection' field is required.");
|
|
70
70
|
const rows = await db.runReadOnly(connection, "SHOW DATABASES");
|
|
71
71
|
return ok(rows);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
case "list_tables": {
|
|
75
75
|
const { connection, database } = args || {};
|
|
76
|
-
if (!connection) return fail("'connection'
|
|
76
|
+
if (!connection) return fail("'connection' field is required.");
|
|
77
77
|
const sql = database
|
|
78
78
|
? `SHOW TABLES FROM \`${database}\``
|
|
79
79
|
: "SHOW TABLES";
|
|
@@ -83,8 +83,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
83
83
|
|
|
84
84
|
case "describe_table": {
|
|
85
85
|
const { connection, table, database } = args || {};
|
|
86
|
-
if (!connection) return fail("'connection'
|
|
87
|
-
if (!table) return fail("'table'
|
|
86
|
+
if (!connection) return fail("'connection' field is required.");
|
|
87
|
+
if (!table) return fail("'table' field is required.");
|
|
88
88
|
const path = database ? `\`${database}\`.\`${table}\`` : `\`${table}\``;
|
|
89
89
|
const rows = await db.runReadOnly(connection, `DESCRIBE ${path}`);
|
|
90
90
|
return ok(rows);
|
|
@@ -96,8 +96,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
96
96
|
const database = args?.database;
|
|
97
97
|
const requestedRowLimit = args?.row_limit;
|
|
98
98
|
|
|
99
|
-
if (!connection) return fail("'connection'
|
|
100
|
-
if (!query) return fail("'query'
|
|
99
|
+
if (!connection) return fail("'connection' field is required.");
|
|
100
|
+
if (!query) return fail("'query' field is required.");
|
|
101
101
|
|
|
102
102
|
const connectionConfig = db.getConnectionConfig(connection);
|
|
103
103
|
const validatedQuery = validateReadQuery(query);
|
|
@@ -130,8 +130,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
130
130
|
|
|
131
131
|
case "execute_write": {
|
|
132
132
|
const { connection, query } = args || {};
|
|
133
|
-
if (!connection) return fail("'connection'
|
|
134
|
-
if (!query) return fail("'query'
|
|
133
|
+
if (!connection) return fail("'connection' field is required.");
|
|
134
|
+
if (!query) return fail("'query' field is required.");
|
|
135
135
|
|
|
136
136
|
const validatedQuery = validateWriteQuery(query);
|
|
137
137
|
const meta = await db.runWrite(connection, validatedQuery);
|
|
@@ -140,9 +140,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
140
140
|
|
|
141
141
|
case "execute_transaction": {
|
|
142
142
|
const { connection, queries } = args || {};
|
|
143
|
-
if (!connection) return fail("'connection'
|
|
143
|
+
if (!connection) return fail("'connection' field is required.");
|
|
144
144
|
if (!Array.isArray(queries) || queries.length === 0) {
|
|
145
|
-
return fail("'queries'
|
|
145
|
+
return fail("'queries' must be an array with at least one query.");
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
for (const q of queries) {
|
|
@@ -156,19 +156,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
156
156
|
case "suggest_query": {
|
|
157
157
|
const { connection, query, reason } = args || {};
|
|
158
158
|
return ok({
|
|
159
|
-
message: "
|
|
159
|
+
message: "MANUAL EXECUTION REQUIRED",
|
|
160
160
|
connection,
|
|
161
|
-
reason: reason || "
|
|
161
|
+
reason: reason || "Write operation",
|
|
162
162
|
query,
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
default:
|
|
167
|
-
return fail(`
|
|
167
|
+
return fail(`Unknown tool: ${name}`);
|
|
168
168
|
}
|
|
169
169
|
} catch (error) {
|
|
170
170
|
const message = error instanceof Error ? error.message : String(error);
|
|
171
|
-
return fail(`
|
|
171
|
+
return fail(`Error: ${message}`);
|
|
172
172
|
}
|
|
173
173
|
});
|
|
174
174
|
|
|
@@ -179,7 +179,7 @@ async function main() {
|
|
|
179
179
|
|
|
180
180
|
main().catch((error) => {
|
|
181
181
|
const message = error instanceof Error ? error.message : String(error);
|
|
182
|
-
console.error(`
|
|
182
|
+
console.error(`Server failed to start: ${message}`);
|
|
183
183
|
process.exit(1);
|
|
184
184
|
});
|
|
185
185
|
|
package/src/sqlGuard.js
CHANGED
|
@@ -28,17 +28,17 @@ export function isWriteQuery(sql) {
|
|
|
28
28
|
|
|
29
29
|
export function validateReadQuery(rawQuery) {
|
|
30
30
|
if (typeof rawQuery !== "string") {
|
|
31
|
-
throw new Error("
|
|
31
|
+
throw new Error("Query must be a string.");
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const query = rawQuery.trim();
|
|
35
35
|
if (!query) {
|
|
36
|
-
throw new Error("
|
|
36
|
+
throw new Error("Query must not be empty.");
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
if (!isReadQuery(query)) {
|
|
40
40
|
throw new Error(
|
|
41
|
-
"
|
|
41
|
+
"Only SELECT/SHOW/DESCRIBE/EXPLAIN queries are allowed."
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -47,23 +47,23 @@ export function validateReadQuery(rawQuery) {
|
|
|
47
47
|
|
|
48
48
|
export function validateWriteQuery(rawQuery) {
|
|
49
49
|
if (typeof rawQuery !== "string") {
|
|
50
|
-
throw new Error("
|
|
50
|
+
throw new Error("Query must be a string.");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const query = rawQuery.trim();
|
|
54
54
|
if (!query) {
|
|
55
|
-
throw new Error("
|
|
55
|
+
throw new Error("Query must not be empty.");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
if (isReadQuery(query)) {
|
|
59
59
|
throw new Error(
|
|
60
|
-
"Read-only
|
|
60
|
+
"Read-only queries (SELECT/SHOW/DESCRIBE/EXPLAIN) cannot be used with execute_write. Use execute_select instead."
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
if (!isWriteQuery(query)) {
|
|
65
65
|
throw new Error(
|
|
66
|
-
`
|
|
66
|
+
`Query was not recognized as a valid write operation. Allowed prefixes: ${WRITE_PREFIXES.join(", ")}`
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -74,9 +74,9 @@ export function validateWriteQuery(rawQuery) {
|
|
|
74
74
|
* Resolves the effective row limit for a query.
|
|
75
75
|
* Returns null when no limit should be applied.
|
|
76
76
|
*
|
|
77
|
-
* -
|
|
78
|
-
* -
|
|
79
|
-
* -
|
|
77
|
+
* - If default_row_limit / max_row_limit are absent (0) → unlimited
|
|
78
|
+
* - If the caller provides row_limit and max_row_limit is set → min(requested, max) is used
|
|
79
|
+
* - If the caller omits row_limit but default_row_limit is set → default is used
|
|
80
80
|
*/
|
|
81
81
|
export function resolveRowLimit(requestedRowLimit, connectionConfig) {
|
|
82
82
|
const defaultLimit = connectionConfig.default_row_limit || 0;
|
|
@@ -85,7 +85,7 @@ export function resolveRowLimit(requestedRowLimit, connectionConfig) {
|
|
|
85
85
|
if (requestedRowLimit != null) {
|
|
86
86
|
const n = Number(requestedRowLimit);
|
|
87
87
|
if (!Number.isFinite(n) || n <= 0) {
|
|
88
|
-
throw new Error("row_limit
|
|
88
|
+
throw new Error("row_limit must be a positive number.");
|
|
89
89
|
}
|
|
90
90
|
const chosen = Math.floor(n);
|
|
91
91
|
return maxLimit > 0 ? Math.min(chosen, maxLimit) : chosen;
|
package/src/tools.js
CHANGED
|
@@ -6,12 +6,12 @@ export function buildToolDefinitions(
|
|
|
6
6
|
const tools = [
|
|
7
7
|
{
|
|
8
8
|
name: "list_connections",
|
|
9
|
-
description: "
|
|
9
|
+
description: "Lists configured MariaDB connections.",
|
|
10
10
|
inputSchema: { type: "object", properties: {} },
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
name: "list_databases",
|
|
14
|
-
description: "
|
|
14
|
+
description: "Lists databases for the selected connection.",
|
|
15
15
|
inputSchema: {
|
|
16
16
|
type: "object",
|
|
17
17
|
properties: {
|
|
@@ -22,7 +22,7 @@ export function buildToolDefinitions(
|
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
name: "list_tables",
|
|
25
|
-
description: "
|
|
25
|
+
description: "Lists tables for the selected connection.",
|
|
26
26
|
inputSchema: {
|
|
27
27
|
type: "object",
|
|
28
28
|
properties: {
|
|
@@ -34,7 +34,7 @@ export function buildToolDefinitions(
|
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
name: "describe_table",
|
|
37
|
-
description: "
|
|
37
|
+
description: "Returns column information for a table.",
|
|
38
38
|
inputSchema: {
|
|
39
39
|
type: "object",
|
|
40
40
|
properties: {
|
|
@@ -48,7 +48,7 @@ export function buildToolDefinitions(
|
|
|
48
48
|
{
|
|
49
49
|
name: "execute_select",
|
|
50
50
|
description:
|
|
51
|
-
"
|
|
51
|
+
"Executes a read-only SELECT/SHOW/DESCRIBE/EXPLAIN query. Row limit is enforced.",
|
|
52
52
|
inputSchema: {
|
|
53
53
|
type: "object",
|
|
54
54
|
properties: {
|
|
@@ -66,7 +66,7 @@ export function buildToolDefinitions(
|
|
|
66
66
|
tools.push(
|
|
67
67
|
{
|
|
68
68
|
name: "execute_write",
|
|
69
|
-
description: `INSERT/UPDATE/DELETE/CREATE/ALTER/DROP
|
|
69
|
+
description: `Executes a write query (INSERT/UPDATE/DELETE/CREATE/ALTER/DROP etc.). Connections: ${writableConnections.join(", ")}`,
|
|
70
70
|
inputSchema: {
|
|
71
71
|
type: "object",
|
|
72
72
|
properties: {
|
|
@@ -78,7 +78,7 @@ export function buildToolDefinitions(
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
name: "execute_transaction",
|
|
81
|
-
description: `
|
|
81
|
+
description: `Executes multiple write queries within a transaction. Connections: ${writableConnections.join(", ")}`,
|
|
82
82
|
inputSchema: {
|
|
83
83
|
type: "object",
|
|
84
84
|
properties: {
|
|
@@ -97,7 +97,7 @@ export function buildToolDefinitions(
|
|
|
97
97
|
|
|
98
98
|
tools.push({
|
|
99
99
|
name: "suggest_query",
|
|
100
|
-
description: "
|
|
100
|
+
description: "Suggests a query that should be executed manually.",
|
|
101
101
|
inputSchema: {
|
|
102
102
|
type: "object",
|
|
103
103
|
properties: {
|