db-mcp-server 1.0.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ing. Luca Stucchi
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.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: db-mcp-server
3
+ Version: 1.0.0
4
+ Summary: MCP server for MySQL and MongoDB databases — one instance per database, no Docker required
5
+ Author: Ing. Luca Stucchi
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/stucchi/db-mcp-server
8
+ Project-URL: Repository, https://github.com/stucchi/db-mcp-server
9
+ Project-URL: Issues, https://github.com/stucchi/db-mcp-server/issues
10
+ Keywords: mcp,database,mysql,mongodb,ai
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Database
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: mcp[cli]
23
+ Requires-Dist: aiomysql
24
+ Requires-Dist: motor
25
+ Dynamic: license-file
26
+
27
+ # db-mcp-server
28
+
29
+ MCP server for MySQL and MongoDB databases. One instance per database, no Docker required.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ uvx db-mcp-server
35
+ ```
36
+
37
+ ## Configuration
38
+
39
+ Configure via environment variables. Each instance connects to a single database.
40
+
41
+ ### MySQL
42
+
43
+ | Variable | Required | Default | Description |
44
+ |----------|----------|---------|-------------|
45
+ | `DB_TYPE` | Yes | — | `mysql` |
46
+ | `DB_DATABASE` | Yes | — | Database name |
47
+ | `DB_PASSWORD` | Yes | — | Password |
48
+ | `DB_HOST` | No | `localhost` | Host |
49
+ | `DB_PORT` | No | `3306` | Port |
50
+ | `DB_USER` | No | `root` | User |
51
+ | `DB_MODE` | No | `read-only` | `read-only` or `read-write` |
52
+
53
+ ### MongoDB
54
+
55
+ | Variable | Required | Default | Description |
56
+ |----------|----------|---------|-------------|
57
+ | `DB_TYPE` | Yes | — | `mongodb` |
58
+ | `DB_DATABASE` | Yes | — | Database name |
59
+ | `DB_URL` | Yes | — | Connection URL (`mongodb://...`) |
60
+ | `DB_MODE` | No | `read-only` | `read-only` or `read-write` |
61
+
62
+ ## Usage in .mcp.json
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "db-prod": {
68
+ "command": "uvx",
69
+ "args": ["db-mcp-server"],
70
+ "env": {
71
+ "DB_TYPE": "mysql",
72
+ "DB_MODE": "read-only",
73
+ "DB_HOST": "db.example.com",
74
+ "DB_PORT": "3306",
75
+ "DB_USER": "root",
76
+ "DB_PASSWORD": "secret",
77
+ "DB_DATABASE": "myapp"
78
+ }
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ For multiple databases, add multiple instances:
85
+
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "db-prod": {
90
+ "command": "uvx",
91
+ "args": ["db-mcp-server"],
92
+ "env": { "DB_TYPE": "mysql", "DB_DATABASE": "prod", "..." : "..." }
93
+ },
94
+ "db-staging": {
95
+ "command": "uvx",
96
+ "args": ["db-mcp-server"],
97
+ "env": { "DB_TYPE": "mongodb", "DB_DATABASE": "staging", "..." : "..." }
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ ## Tools
104
+
105
+ ### MySQL
106
+
107
+ - **query** — Execute read-only SQL (SELECT, SHOW, DESCRIBE, EXPLAIN, WITH)
108
+ - **execute** — Execute write SQL (INSERT, UPDATE, DELETE) — requires `DB_MODE=read-write`
109
+ - **describe** — Describe table structure
110
+ - **list_tables** — List all tables
111
+ - **status** — Show connection info
112
+
113
+ ### MongoDB
114
+
115
+ - **query** — Find documents in a collection
116
+ - **describe** — Collection stats ($collStats)
117
+ - **list_collections** — List all collections
118
+ - **aggregate** — Execute aggregation pipelines ($out/$merge blocked on read-only)
119
+ - **status** — Show connection info
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,97 @@
1
+ # db-mcp-server
2
+
3
+ MCP server for MySQL and MongoDB databases. One instance per database, no Docker required.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uvx db-mcp-server
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ Configure via environment variables. Each instance connects to a single database.
14
+
15
+ ### MySQL
16
+
17
+ | Variable | Required | Default | Description |
18
+ |----------|----------|---------|-------------|
19
+ | `DB_TYPE` | Yes | — | `mysql` |
20
+ | `DB_DATABASE` | Yes | — | Database name |
21
+ | `DB_PASSWORD` | Yes | — | Password |
22
+ | `DB_HOST` | No | `localhost` | Host |
23
+ | `DB_PORT` | No | `3306` | Port |
24
+ | `DB_USER` | No | `root` | User |
25
+ | `DB_MODE` | No | `read-only` | `read-only` or `read-write` |
26
+
27
+ ### MongoDB
28
+
29
+ | Variable | Required | Default | Description |
30
+ |----------|----------|---------|-------------|
31
+ | `DB_TYPE` | Yes | — | `mongodb` |
32
+ | `DB_DATABASE` | Yes | — | Database name |
33
+ | `DB_URL` | Yes | — | Connection URL (`mongodb://...`) |
34
+ | `DB_MODE` | No | `read-only` | `read-only` or `read-write` |
35
+
36
+ ## Usage in .mcp.json
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "db-prod": {
42
+ "command": "uvx",
43
+ "args": ["db-mcp-server"],
44
+ "env": {
45
+ "DB_TYPE": "mysql",
46
+ "DB_MODE": "read-only",
47
+ "DB_HOST": "db.example.com",
48
+ "DB_PORT": "3306",
49
+ "DB_USER": "root",
50
+ "DB_PASSWORD": "secret",
51
+ "DB_DATABASE": "myapp"
52
+ }
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ For multiple databases, add multiple instances:
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "db-prod": {
64
+ "command": "uvx",
65
+ "args": ["db-mcp-server"],
66
+ "env": { "DB_TYPE": "mysql", "DB_DATABASE": "prod", "..." : "..." }
67
+ },
68
+ "db-staging": {
69
+ "command": "uvx",
70
+ "args": ["db-mcp-server"],
71
+ "env": { "DB_TYPE": "mongodb", "DB_DATABASE": "staging", "..." : "..." }
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## Tools
78
+
79
+ ### MySQL
80
+
81
+ - **query** — Execute read-only SQL (SELECT, SHOW, DESCRIBE, EXPLAIN, WITH)
82
+ - **execute** — Execute write SQL (INSERT, UPDATE, DELETE) — requires `DB_MODE=read-write`
83
+ - **describe** — Describe table structure
84
+ - **list_tables** — List all tables
85
+ - **status** — Show connection info
86
+
87
+ ### MongoDB
88
+
89
+ - **query** — Find documents in a collection
90
+ - **describe** — Collection stats ($collStats)
91
+ - **list_collections** — List all collections
92
+ - **aggregate** — Execute aggregation pipelines ($out/$merge blocked on read-only)
93
+ - **status** — Show connection info
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "db-mcp-server"
7
+ version = "1.0.0"
8
+ description = "MCP server for MySQL and MongoDB databases — one instance per database, no Docker required"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Ing. Luca Stucchi" },
14
+ ]
15
+ keywords = ["mcp", "database", "mysql", "mongodb", "ai"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Database",
25
+ ]
26
+ dependencies = [
27
+ "mcp[cli]",
28
+ "aiomysql",
29
+ "motor",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/stucchi/db-mcp-server"
34
+ Repository = "https://github.com/stucchi/db-mcp-server"
35
+ Issues = "https://github.com/stucchi/db-mcp-server/issues"
36
+
37
+ [project.scripts]
38
+ db-mcp-server = "db_mcp.server:main"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,3 @@
1
+ from db_mcp.server import main
2
+
3
+ main()
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Config:
9
+ db_type: str # "mysql" or "mongodb"
10
+ db_mode: str # "read-only" or "read-write"
11
+ db_database: str
12
+
13
+ # MySQL
14
+ db_host: str
15
+ db_port: int
16
+ db_user: str
17
+ db_password: str
18
+
19
+ # MongoDB
20
+ db_url: str
21
+
22
+ @property
23
+ def is_mysql(self) -> bool:
24
+ return self.db_type == "mysql"
25
+
26
+ @property
27
+ def is_mongodb(self) -> bool:
28
+ return self.db_type == "mongodb"
29
+
30
+ @property
31
+ def is_read_only(self) -> bool:
32
+ return self.db_mode == "read-only"
33
+
34
+ @staticmethod
35
+ def from_env() -> Config:
36
+ db_type = os.environ.get("DB_TYPE", "").lower()
37
+ db_mode = os.environ.get("DB_MODE", "read-only").lower()
38
+ db_database = os.environ.get("DB_DATABASE", "")
39
+
40
+ missing: list[str] = []
41
+
42
+ if db_type not in ("mysql", "mongodb"):
43
+ raise RuntimeError(
44
+ "DB_TYPE must be 'mysql' or 'mongodb'.\n"
45
+ "Set DB_TYPE in your environment variables."
46
+ )
47
+
48
+ if not db_database:
49
+ missing.append("DB_DATABASE")
50
+
51
+ if db_mode not in ("read-only", "read-write"):
52
+ raise RuntimeError(
53
+ "DB_MODE must be 'read-only' or 'read-write'.\n"
54
+ f"Got: '{db_mode}'"
55
+ )
56
+
57
+ # MySQL-specific
58
+ db_host = os.environ.get("DB_HOST", "localhost")
59
+ db_port = int(os.environ.get("DB_PORT", "3306"))
60
+ db_user = os.environ.get("DB_USER", "root")
61
+ db_password = os.environ.get("DB_PASSWORD", "")
62
+ db_url = os.environ.get("DB_URL", "")
63
+
64
+ if db_type == "mysql" and not db_password:
65
+ missing.append("DB_PASSWORD")
66
+
67
+ if db_type == "mongodb" and not db_url:
68
+ missing.append("DB_URL")
69
+
70
+ if missing:
71
+ raise RuntimeError(
72
+ f"Missing required environment variables: {', '.join(missing)}.\n"
73
+ "MySQL requires: DB_TYPE, DB_DATABASE, DB_PASSWORD\n"
74
+ "MongoDB requires: DB_TYPE, DB_DATABASE, DB_URL"
75
+ )
76
+
77
+ return Config(
78
+ db_type=db_type,
79
+ db_mode=db_mode,
80
+ db_database=db_database,
81
+ db_host=db_host,
82
+ db_port=db_port,
83
+ db_user=db_user,
84
+ db_password=db_password,
85
+ db_url=db_url,
86
+ )
87
+
88
+
89
+ _config: Config | None = None
90
+
91
+
92
+ def get_config() -> Config:
93
+ global _config
94
+ if _config is None:
95
+ _config = Config.from_env()
96
+ return _config
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import Any
5
+
6
+ import aiomysql
7
+ import motor.motor_asyncio
8
+
9
+ from db_mcp.config import Config
10
+
11
+
12
+ class Connection:
13
+ def __init__(self, config: Config) -> None:
14
+ self.config = config
15
+ self._pool: aiomysql.Pool | None = None
16
+ self._mongo_client: motor.motor_asyncio.AsyncIOMotorClient | None = None
17
+ self._mongo_db: Any = None
18
+
19
+ @property
20
+ def pool(self) -> aiomysql.Pool:
21
+ assert self._pool is not None, "MySQL pool not initialized"
22
+ return self._pool
23
+
24
+ @property
25
+ def db(self) -> Any:
26
+ assert self._mongo_db is not None, "MongoDB database not initialized"
27
+ return self._mongo_db
28
+
29
+ async def connect(self) -> None:
30
+ if self.config.is_mysql:
31
+ await self._connect_mysql()
32
+ else:
33
+ await self._connect_mongodb()
34
+
35
+ async def _connect_mysql(self) -> None:
36
+ print(
37
+ f"[db-mcp] Connecting to MySQL {self.config.db_host}:{self.config.db_port}"
38
+ f"/{self.config.db_database} ({self.config.db_mode})...",
39
+ file=sys.stderr,
40
+ )
41
+ self._pool = await aiomysql.create_pool(
42
+ host=self.config.db_host,
43
+ port=self.config.db_port,
44
+ user=self.config.db_user,
45
+ password=self.config.db_password,
46
+ db=self.config.db_database,
47
+ autocommit=True,
48
+ )
49
+ # Verify connectivity
50
+ async with self._pool.acquire() as conn:
51
+ await conn.ping()
52
+ print("[db-mcp] MySQL connected.", file=sys.stderr)
53
+
54
+ async def _connect_mongodb(self) -> None:
55
+ print(
56
+ f"[db-mcp] Connecting to MongoDB {self.config.db_database} "
57
+ f"({self.config.db_mode})...",
58
+ file=sys.stderr,
59
+ )
60
+ self._mongo_client = motor.motor_asyncio.AsyncIOMotorClient(self.config.db_url)
61
+ self._mongo_db = self._mongo_client[self.config.db_database]
62
+ # Verify connectivity
63
+ await self._mongo_db.command("ping")
64
+ print("[db-mcp] MongoDB connected.", file=sys.stderr)
65
+
66
+ async def close(self) -> None:
67
+ if self._pool is not None:
68
+ self._pool.close()
69
+ await self._pool.wait_closed()
70
+ print("[db-mcp] MySQL disconnected.", file=sys.stderr)
71
+ if self._mongo_client is not None:
72
+ self._mongo_client.close()
73
+ print("[db-mcp] MongoDB disconnected.", file=sys.stderr)
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import AsyncIterator
5
+ from contextlib import asynccontextmanager
6
+ from typing import Annotated, Any
7
+
8
+ from mcp.server.fastmcp import FastMCP
9
+
10
+ from db_mcp.config import get_config
11
+ from db_mcp.connection import Connection
12
+ from db_mcp.tools.aggregate import aggregate_mongodb
13
+ from db_mcp.tools.describe import describe_mongodb, describe_mysql
14
+ from db_mcp.tools.execute import execute_mysql
15
+ from db_mcp.tools.list_collections import list_collections as _list_collections
16
+ from db_mcp.tools.list_tables import list_tables as _list_tables
17
+ from db_mcp.tools.query import query_mongodb, query_mysql
18
+ from db_mcp.tools.status import get_status
19
+
20
+ config = get_config()
21
+ _conn = Connection(config)
22
+
23
+
24
+ def _format(result: Any) -> str:
25
+ return json.dumps(result, indent=2, ensure_ascii=False, default=str)
26
+
27
+
28
+ @asynccontextmanager
29
+ async def app_lifespan(server: FastMCP) -> AsyncIterator[None]:
30
+ await _conn.connect()
31
+ try:
32
+ yield
33
+ finally:
34
+ await _conn.close()
35
+
36
+
37
+ mcp = FastMCP(
38
+ f"db-mcp-server ({config.db_type}:{config.db_database})",
39
+ lifespan=app_lifespan,
40
+ )
41
+
42
+
43
+ # --- Tool: query ---
44
+
45
+ if config.is_mysql:
46
+
47
+ @mcp.tool()
48
+ async def query(
49
+ query: Annotated[str, "SQL SELECT query to execute"],
50
+ ) -> str:
51
+ """Execute a read-only query on the MySQL database. Only SELECT, SHOW, DESCRIBE, EXPLAIN, WITH are allowed on read-only databases."""
52
+ rows = await query_mysql(_conn, config, query)
53
+ return _format(rows)
54
+
55
+ else:
56
+
57
+ @mcp.tool()
58
+ async def query(
59
+ collection: Annotated[str, "Collection name to query"],
60
+ filter: Annotated[dict | None, "MongoDB filter object (default: {})"] = None,
61
+ limit: Annotated[int, "Maximum number of results (default: 100, max: 1000)"] = 100,
62
+ ) -> str:
63
+ """Execute a find query on a MongoDB collection."""
64
+ rows = await query_mongodb(_conn, collection, filter, limit)
65
+ return _format(rows)
66
+
67
+
68
+ # --- Tool: execute (MySQL only) ---
69
+
70
+ if config.is_mysql:
71
+
72
+ @mcp.tool()
73
+ async def execute(
74
+ query: Annotated[str, "SQL query to execute (INSERT, UPDATE, DELETE, etc.)"],
75
+ ) -> str:
76
+ """Execute a write query on the MySQL database. Only works if the database is configured with mode='read-write'."""
77
+ result = await execute_mysql(_conn, config, query)
78
+ return _format(result)
79
+
80
+
81
+ # --- Tool: describe ---
82
+
83
+ if config.is_mysql:
84
+
85
+ @mcp.tool()
86
+ async def describe(
87
+ table: Annotated[str, "Table name to describe"],
88
+ ) -> str:
89
+ """Describe the structure of a MySQL table (DESCRIBE)."""
90
+ rows = await describe_mysql(_conn, table)
91
+ return _format(rows)
92
+
93
+ else:
94
+
95
+ @mcp.tool()
96
+ async def describe(
97
+ collection: Annotated[str, "Collection name to describe"],
98
+ ) -> str:
99
+ """Describe the structure of a MongoDB collection ($collStats)."""
100
+ rows = await describe_mongodb(_conn, collection)
101
+ return _format(rows)
102
+
103
+
104
+ # --- Tool: list_tables (MySQL only) ---
105
+
106
+ if config.is_mysql:
107
+
108
+ @mcp.tool()
109
+ async def list_tables() -> str:
110
+ """List all tables in the MySQL database."""
111
+ rows = await _list_tables(_conn)
112
+ return _format(rows)
113
+
114
+
115
+ # --- Tool: list_collections (MongoDB only) ---
116
+
117
+ if config.is_mongodb:
118
+
119
+ @mcp.tool()
120
+ async def list_collections() -> str:
121
+ """List all collections in the MongoDB database."""
122
+ names = await _list_collections(_conn)
123
+ return _format(names)
124
+
125
+
126
+ # --- Tool: aggregate (MongoDB only) ---
127
+
128
+ if config.is_mongodb:
129
+
130
+ @mcp.tool()
131
+ async def aggregate(
132
+ collection: Annotated[str, "Collection name to aggregate"],
133
+ pipeline: Annotated[list[dict[str, Any]], "MongoDB aggregation pipeline array"],
134
+ ) -> str:
135
+ """Execute an aggregation pipeline on a MongoDB collection. Pipelines with $out/$merge are blocked on read-only databases."""
136
+ rows = await aggregate_mongodb(_conn, config, collection, pipeline)
137
+ return _format(rows)
138
+
139
+
140
+ # --- Tool: status ---
141
+
142
+
143
+ @mcp.tool()
144
+ async def status() -> str:
145
+ """Show connection info: type, host, database, mode, status."""
146
+ return _format(get_status(config))
147
+
148
+
149
+ def main() -> None:
150
+ mcp.run(transport="stdio")
File without changes
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from db_mcp.config import Config
6
+ from db_mcp.connection import Connection
7
+ from db_mcp.validation import sanitize_table_name, validate_aggregate_pipeline
8
+
9
+
10
+ async def aggregate_mongodb(
11
+ conn: Connection,
12
+ config: Config,
13
+ collection: str,
14
+ pipeline: list[dict[str, Any]],
15
+ ) -> list[dict]:
16
+ validate_aggregate_pipeline(pipeline, config.is_read_only)
17
+ safe_name = sanitize_table_name(collection)
18
+ cursor = conn.db[safe_name].aggregate(pipeline)
19
+ return await cursor.to_list(1000)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import aiomysql
4
+
5
+ from db_mcp.connection import Connection
6
+ from db_mcp.validation import sanitize_table_name
7
+
8
+
9
+ async def describe_mysql(conn: Connection, table: str) -> list[dict]:
10
+ safe_name = sanitize_table_name(table)
11
+ async with conn.pool.acquire() as c:
12
+ async with c.cursor(aiomysql.DictCursor) as cur:
13
+ await cur.execute(f"DESCRIBE {safe_name}")
14
+ return await cur.fetchall()
15
+
16
+
17
+ async def describe_mongodb(conn: Connection, collection: str) -> list[dict]:
18
+ safe_name = sanitize_table_name(collection)
19
+ cursor = conn.db[safe_name].aggregate([{"$collStats": {"storageStats": {}}}])
20
+ return await cursor.to_list(10)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from db_mcp.config import Config
4
+ from db_mcp.connection import Connection
5
+
6
+
7
+ async def execute_mysql(conn: Connection, config: Config, sql: str) -> dict:
8
+ if config.is_read_only:
9
+ raise ValueError(
10
+ "This database is in READ-ONLY mode. "
11
+ "Write operations are not allowed."
12
+ )
13
+
14
+ async with conn.pool.acquire() as c:
15
+ async with c.cursor() as cur:
16
+ await cur.execute(sql)
17
+ return {
18
+ "affectedRows": cur.rowcount,
19
+ "insertId": cur.lastrowid or 0,
20
+ }
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from db_mcp.connection import Connection
4
+
5
+
6
+ async def list_collections(conn: Connection) -> list[str]:
7
+ return await conn.db.list_collection_names()
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ import aiomysql
4
+
5
+ from db_mcp.connection import Connection
6
+
7
+
8
+ async def list_tables(conn: Connection) -> list[dict]:
9
+ async with conn.pool.acquire() as c:
10
+ async with c.cursor(aiomysql.DictCursor) as cur:
11
+ await cur.execute("SHOW TABLES")
12
+ return await cur.fetchall()
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import aiomysql
6
+
7
+ from db_mcp.config import Config
8
+ from db_mcp.connection import Connection
9
+ from db_mcp.validation import validate_read_only_query
10
+
11
+
12
+ async def query_mysql(conn: Connection, config: Config, sql: str) -> list[dict]:
13
+ if config.is_read_only:
14
+ validate_read_only_query(sql)
15
+
16
+ async with conn.pool.acquire() as c:
17
+ async with c.cursor(aiomysql.DictCursor) as cur:
18
+ await cur.execute(sql)
19
+ rows = await cur.fetchall()
20
+ return rows
21
+
22
+
23
+ async def query_mongodb(
24
+ conn: Connection,
25
+ collection: str,
26
+ filter_obj: dict[str, Any] | None = None,
27
+ limit: int = 100,
28
+ ) -> list[dict]:
29
+ if filter_obj is None:
30
+ filter_obj = {}
31
+ capped = min(limit, 1000)
32
+ results = await conn.db[collection].find(filter_obj).limit(capped).to_list(capped)
33
+ return results
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from db_mcp.config import Config
4
+
5
+
6
+ def get_status(config: Config) -> dict:
7
+ info: dict = {
8
+ "type": config.db_type,
9
+ "database": config.db_database,
10
+ "mode": config.db_mode,
11
+ "status": "connected",
12
+ }
13
+ if config.is_mysql:
14
+ info["host"] = f"{config.db_host}:{config.db_port}"
15
+ info["user"] = config.db_user
16
+ else:
17
+ info["url"] = config.db_url
18
+ return info
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ _READ_ONLY_PREFIXES = ("select", "show", "describe", "explain", "with")
6
+
7
+
8
+ def validate_read_only_query(sql: str) -> None:
9
+ trimmed = sql.strip().lower()
10
+ if not any(trimmed.startswith(p) for p in _READ_ONLY_PREFIXES):
11
+ raise ValueError(
12
+ "Only SELECT, SHOW, DESCRIBE, EXPLAIN, WITH queries are allowed "
13
+ "on read-only databases."
14
+ )
15
+
16
+
17
+ def sanitize_table_name(name: str) -> str:
18
+ sanitized = re.sub(r"[^a-zA-Z0-9_]", "", name)
19
+ if not sanitized:
20
+ raise ValueError(f"Invalid table/collection name: {name!r}")
21
+ return sanitized
22
+
23
+
24
+ def validate_aggregate_pipeline(pipeline: list, read_only: bool) -> None:
25
+ if not read_only:
26
+ return
27
+ for stage in pipeline:
28
+ if not isinstance(stage, dict):
29
+ continue
30
+ for key in stage:
31
+ if key in ("$out", "$merge"):
32
+ raise ValueError(
33
+ f"Aggregation pipelines with {key} are not allowed "
34
+ "on read-only databases."
35
+ )
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: db-mcp-server
3
+ Version: 1.0.0
4
+ Summary: MCP server for MySQL and MongoDB databases — one instance per database, no Docker required
5
+ Author: Ing. Luca Stucchi
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/stucchi/db-mcp-server
8
+ Project-URL: Repository, https://github.com/stucchi/db-mcp-server
9
+ Project-URL: Issues, https://github.com/stucchi/db-mcp-server/issues
10
+ Keywords: mcp,database,mysql,mongodb,ai
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Database
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: mcp[cli]
23
+ Requires-Dist: aiomysql
24
+ Requires-Dist: motor
25
+ Dynamic: license-file
26
+
27
+ # db-mcp-server
28
+
29
+ MCP server for MySQL and MongoDB databases. One instance per database, no Docker required.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ uvx db-mcp-server
35
+ ```
36
+
37
+ ## Configuration
38
+
39
+ Configure via environment variables. Each instance connects to a single database.
40
+
41
+ ### MySQL
42
+
43
+ | Variable | Required | Default | Description |
44
+ |----------|----------|---------|-------------|
45
+ | `DB_TYPE` | Yes | — | `mysql` |
46
+ | `DB_DATABASE` | Yes | — | Database name |
47
+ | `DB_PASSWORD` | Yes | — | Password |
48
+ | `DB_HOST` | No | `localhost` | Host |
49
+ | `DB_PORT` | No | `3306` | Port |
50
+ | `DB_USER` | No | `root` | User |
51
+ | `DB_MODE` | No | `read-only` | `read-only` or `read-write` |
52
+
53
+ ### MongoDB
54
+
55
+ | Variable | Required | Default | Description |
56
+ |----------|----------|---------|-------------|
57
+ | `DB_TYPE` | Yes | — | `mongodb` |
58
+ | `DB_DATABASE` | Yes | — | Database name |
59
+ | `DB_URL` | Yes | — | Connection URL (`mongodb://...`) |
60
+ | `DB_MODE` | No | `read-only` | `read-only` or `read-write` |
61
+
62
+ ## Usage in .mcp.json
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "db-prod": {
68
+ "command": "uvx",
69
+ "args": ["db-mcp-server"],
70
+ "env": {
71
+ "DB_TYPE": "mysql",
72
+ "DB_MODE": "read-only",
73
+ "DB_HOST": "db.example.com",
74
+ "DB_PORT": "3306",
75
+ "DB_USER": "root",
76
+ "DB_PASSWORD": "secret",
77
+ "DB_DATABASE": "myapp"
78
+ }
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ For multiple databases, add multiple instances:
85
+
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "db-prod": {
90
+ "command": "uvx",
91
+ "args": ["db-mcp-server"],
92
+ "env": { "DB_TYPE": "mysql", "DB_DATABASE": "prod", "..." : "..." }
93
+ },
94
+ "db-staging": {
95
+ "command": "uvx",
96
+ "args": ["db-mcp-server"],
97
+ "env": { "DB_TYPE": "mongodb", "DB_DATABASE": "staging", "..." : "..." }
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ ## Tools
104
+
105
+ ### MySQL
106
+
107
+ - **query** — Execute read-only SQL (SELECT, SHOW, DESCRIBE, EXPLAIN, WITH)
108
+ - **execute** — Execute write SQL (INSERT, UPDATE, DELETE) — requires `DB_MODE=read-write`
109
+ - **describe** — Describe table structure
110
+ - **list_tables** — List all tables
111
+ - **status** — Show connection info
112
+
113
+ ### MongoDB
114
+
115
+ - **query** — Find documents in a collection
116
+ - **describe** — Collection stats ($collStats)
117
+ - **list_collections** — List all collections
118
+ - **aggregate** — Execute aggregation pipelines ($out/$merge blocked on read-only)
119
+ - **status** — Show connection info
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,23 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/db_mcp/__init__.py
5
+ src/db_mcp/__main__.py
6
+ src/db_mcp/config.py
7
+ src/db_mcp/connection.py
8
+ src/db_mcp/server.py
9
+ src/db_mcp/validation.py
10
+ src/db_mcp/tools/__init__.py
11
+ src/db_mcp/tools/aggregate.py
12
+ src/db_mcp/tools/describe.py
13
+ src/db_mcp/tools/execute.py
14
+ src/db_mcp/tools/list_collections.py
15
+ src/db_mcp/tools/list_tables.py
16
+ src/db_mcp/tools/query.py
17
+ src/db_mcp/tools/status.py
18
+ src/db_mcp_server.egg-info/PKG-INFO
19
+ src/db_mcp_server.egg-info/SOURCES.txt
20
+ src/db_mcp_server.egg-info/dependency_links.txt
21
+ src/db_mcp_server.egg-info/entry_points.txt
22
+ src/db_mcp_server.egg-info/requires.txt
23
+ src/db_mcp_server.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ db-mcp-server = db_mcp.server:main
@@ -0,0 +1,3 @@
1
+ mcp[cli]
2
+ aiomysql
3
+ motor