mcp-db-server 0.1.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,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-db-server
3
+ Version: 0.1.0
4
+ Summary: MCP Database Server with read-only access
5
+ Author-email: User <user@example.com>
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: mcp
10
+ Requires-Dist: python-dotenv
11
+ Requires-Dist: pymysql
12
+ Requires-Dist: cryptography
13
+ Requires-Dist: DBUtils
14
+
15
+ # MCP Database Server
16
+
17
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server implementation in Python that provides **strictly read-only** access to databases. It allows Large Language Models (LLMs) to inspect schemas and query data safely without risking data modification.
18
+
19
+ ## Features
20
+
21
+ - **🛡️ Strict Read-Only**: Enforced at the driver level (e.g., SQLite `?mode=ro`) or session level (MySQL `SET SESSION TRANSACTION READ ONLY`).
22
+ - **🏗️ Repository Pattern**: Abstracted database access allows for easy extension to other backends (PostgreSQL, MySQL) via Dependency Injection.
23
+ - **⚡ FastMCP**: Built efficiently using the official MCP Python SDK.
24
+ - **🔌 Connection Pooling**: Automatic connection pooling for MySQL using `DBUtils` to ensure performance and reliability.
25
+ - **🔧 Easy Configuration**: Managed via environment variables and Makefiles.
26
+
27
+ ## Project Structure
28
+
29
+ - `server.py`: Main entry point for the MCP server.
30
+ - `db/`: Contains the Repository interface and implementations (`sqlite_repository.py`, `mysql_repository.py`).
31
+ - `seed.py`: Utility script to populate the database (since the server itself is read-only).
32
+ - `Makefile`: Automation for common tasks.
33
+
34
+ ## Quick Start
35
+
36
+ ### Prerequisites
37
+ - Python 3.12+ (managed via `venv`)
38
+ - `make` (optional, but recommended)
39
+ - MySQL Server (optional, if using MySQL backend)
40
+
41
+ ### 1. Setup
42
+ Use the Makefile to create a virtual environment, install dependencies, and seed a test database:
43
+
44
+ ```bash
45
+ make setup
46
+ ```
47
+
48
+ *This runs `pip install -r requirements.txt` and `python seed.py`.*
49
+
50
+ ### 2. Configuration
51
+ The project uses `python-dotenv`. Copy the example config and adjust as needed:
52
+
53
+ ```bash
54
+ cp .env.example .env
55
+ ```
56
+
57
+ **Key Variables:**
58
+ - `DB_ENGINE`: `sqlite` (Default) or `mysql`
59
+ - `DB_ADDRESS`: Path to db file (SQLite) or Host URL (MySQL).
60
+ - `DB_PORT`: Database port (Default: 3306 for MySQL).
61
+ - `DB_USER` / `DB_PASSWORD`: Database credentials (required for MySQL).
62
+ - `DB_SCHEMA`: Database name (required for MySQL).
63
+
64
+ ### 3. Run the Server
65
+ Start the MCP server:
66
+
67
+ ```bash
68
+ make run
69
+ ```
70
+ *(Or directly via `venv/bin/python server.py`)*
71
+
72
+ ## Usage with LLMs
73
+
74
+ Once running, the server exposes the following tools to connected MCP clients:
75
+
76
+ 1. **`list_tables()`**
77
+ - Returns a list of all tables in the database.
78
+ 2. **`describe_table(table_name: str)`**
79
+ - Returns the schema (columns, types, primary keys) for the specified table.
80
+ 3. **`read_query(query: str)`**
81
+ - Executes a SQL `SELECT` query.
82
+ - **Note**: Queries attempting to modify data will raise a `Security Error` or `OperationalError`.
83
+
84
+ ## Development
85
+
86
+ ### Running Tests
87
+ Execute the unit test suite:
88
+
89
+ ```bash
90
+ make test
91
+ ```
92
+
93
+ ### Extending
94
+ To add support for a new database (e.g., Postgres):
95
+ 1. Create `db/postgres_repository.py` implementation of `DatabaseRepository`.
96
+ 2. Update `server.py` to instantiate it when `DB_ENGINE=postgres`.
@@ -0,0 +1,82 @@
1
+ # MCP Database Server
2
+
3
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server implementation in Python that provides **strictly read-only** access to databases. It allows Large Language Models (LLMs) to inspect schemas and query data safely without risking data modification.
4
+
5
+ ## Features
6
+
7
+ - **🛡️ Strict Read-Only**: Enforced at the driver level (e.g., SQLite `?mode=ro`) or session level (MySQL `SET SESSION TRANSACTION READ ONLY`).
8
+ - **🏗️ Repository Pattern**: Abstracted database access allows for easy extension to other backends (PostgreSQL, MySQL) via Dependency Injection.
9
+ - **⚡ FastMCP**: Built efficiently using the official MCP Python SDK.
10
+ - **🔌 Connection Pooling**: Automatic connection pooling for MySQL using `DBUtils` to ensure performance and reliability.
11
+ - **🔧 Easy Configuration**: Managed via environment variables and Makefiles.
12
+
13
+ ## Project Structure
14
+
15
+ - `server.py`: Main entry point for the MCP server.
16
+ - `db/`: Contains the Repository interface and implementations (`sqlite_repository.py`, `mysql_repository.py`).
17
+ - `seed.py`: Utility script to populate the database (since the server itself is read-only).
18
+ - `Makefile`: Automation for common tasks.
19
+
20
+ ## Quick Start
21
+
22
+ ### Prerequisites
23
+ - Python 3.12+ (managed via `venv`)
24
+ - `make` (optional, but recommended)
25
+ - MySQL Server (optional, if using MySQL backend)
26
+
27
+ ### 1. Setup
28
+ Use the Makefile to create a virtual environment, install dependencies, and seed a test database:
29
+
30
+ ```bash
31
+ make setup
32
+ ```
33
+
34
+ *This runs `pip install -r requirements.txt` and `python seed.py`.*
35
+
36
+ ### 2. Configuration
37
+ The project uses `python-dotenv`. Copy the example config and adjust as needed:
38
+
39
+ ```bash
40
+ cp .env.example .env
41
+ ```
42
+
43
+ **Key Variables:**
44
+ - `DB_ENGINE`: `sqlite` (Default) or `mysql`
45
+ - `DB_ADDRESS`: Path to db file (SQLite) or Host URL (MySQL).
46
+ - `DB_PORT`: Database port (Default: 3306 for MySQL).
47
+ - `DB_USER` / `DB_PASSWORD`: Database credentials (required for MySQL).
48
+ - `DB_SCHEMA`: Database name (required for MySQL).
49
+
50
+ ### 3. Run the Server
51
+ Start the MCP server:
52
+
53
+ ```bash
54
+ make run
55
+ ```
56
+ *(Or directly via `venv/bin/python server.py`)*
57
+
58
+ ## Usage with LLMs
59
+
60
+ Once running, the server exposes the following tools to connected MCP clients:
61
+
62
+ 1. **`list_tables()`**
63
+ - Returns a list of all tables in the database.
64
+ 2. **`describe_table(table_name: str)`**
65
+ - Returns the schema (columns, types, primary keys) for the specified table.
66
+ 3. **`read_query(query: str)`**
67
+ - Executes a SQL `SELECT` query.
68
+ - **Note**: Queries attempting to modify data will raise a `Security Error` or `OperationalError`.
69
+
70
+ ## Development
71
+
72
+ ### Running Tests
73
+ Execute the unit test suite:
74
+
75
+ ```bash
76
+ make test
77
+ ```
78
+
79
+ ### Extending
80
+ To add support for a new database (e.g., Postgres):
81
+ 1. Create `db/postgres_repository.py` implementation of `DatabaseRepository`.
82
+ 2. Update `server.py` to instantiate it when `DB_ENGINE=postgres`.
@@ -0,0 +1,116 @@
1
+ import pymysql
2
+ import os
3
+ from typing import List, Union
4
+ from .repository import DatabaseRepository
5
+
6
+ from dbutils.pooled_db import PooledDB
7
+
8
+ class MysqlRepository(DatabaseRepository):
9
+ def __init__(self, host: str, user: str, password: str, database: str, port: int = 3306):
10
+ self.host = host
11
+ self.user = user
12
+ self.password = password
13
+ self.database = database
14
+ self.port = port
15
+
16
+ # Initialize connection pool
17
+ # mincached=2, maxcached=5 are reasonable defaults
18
+ self.pool = PooledDB(
19
+ creator=pymysql,
20
+ mincached=2,
21
+ maxcached=5,
22
+ blocking=True, # Wait for connection if pool is empty
23
+ host=self.host,
24
+ user=self.user,
25
+ password=self.password,
26
+ database=self.database,
27
+ port=self.port,
28
+ cursorclass=pymysql.cursors.Cursor
29
+ )
30
+
31
+ def _get_connection(self):
32
+ # Get connection from pool
33
+ return self.pool.connection()
34
+
35
+ def list_tables(self) -> List[str]:
36
+ try:
37
+ conn = self._get_connection()
38
+ with conn.cursor() as cursor:
39
+ cursor.execute("SHOW TABLES")
40
+ # result is tuple of tuples
41
+ tables = [row[0] for row in cursor.fetchall()]
42
+ conn.close()
43
+ return tables
44
+ except Exception as e:
45
+ return [f"Error listing tables: {e}"]
46
+
47
+ def describe_table(self, table_name: str) -> str:
48
+ try:
49
+ conn = self._get_connection()
50
+ with conn.cursor() as cursor:
51
+ # Validate table exists (prevent injection)
52
+ cursor.execute("SHOW TABLES")
53
+ tables = [row[0] for row in cursor.fetchall()]
54
+ if table_name not in tables:
55
+ return f"Error: Table '{table_name}' does not exist."
56
+
57
+ cursor.execute(f"DESCRIBE {table_name}")
58
+ columns = cursor.fetchall()
59
+ conn.close()
60
+
61
+ schema = []
62
+ # Field, Type, Null, Key, Default, Extra
63
+ for col in columns:
64
+ col_name = col[0]
65
+ col_type = col[1]
66
+ key = col[3]
67
+
68
+ col_str = f"- {col_name} ({col_type})"
69
+ if key == 'PRI':
70
+ col_str += " [PK]"
71
+ schema.append(col_str)
72
+
73
+ return f"Schema for {table_name}:\n" + "\n".join(schema)
74
+ except Exception as e:
75
+ return f"Error describing table: {e}"
76
+
77
+ def read_query(self, query: str) -> str:
78
+ normalized_query = query.strip().upper()
79
+ if not normalized_query.startswith("SELECT"):
80
+ return "Error: Only SELECT queries are allowed."
81
+
82
+ try:
83
+ conn = self._get_connection()
84
+ try:
85
+ with conn.cursor() as cursor:
86
+ # Enforce Read Only mode for this session
87
+ cursor.execute("SET SESSION TRANSACTION READ ONLY")
88
+
89
+ cursor.execute(query)
90
+ results = cursor.fetchall()
91
+
92
+ output = []
93
+ if cursor.description:
94
+ col_names = [desc[0] for desc in cursor.description]
95
+ output.append(" | ".join(col_names))
96
+ output.append("-" * len(" | ".join(col_names)))
97
+
98
+ if not results:
99
+ return "No results found."
100
+
101
+ for row in results:
102
+ row_str = " | ".join(str(item) for item in row)
103
+ output.append(row_str)
104
+
105
+ return "\n".join(output)
106
+ finally:
107
+ conn.close()
108
+
109
+ except pymysql.OperationalError as e:
110
+ # Code 1290 is The MySQL server is running with the --read-only option so it cannot execute this statement
111
+ # Or similar for session read only
112
+ if e.args[0] == 1792: # Cannot execute statement in a READ ONLY transaction.
113
+ return "Security Error: Attempted write operation in read-only mode."
114
+ return f"Database Error: {e}"
115
+ except Exception as e:
116
+ return f"Error: {e}"
@@ -0,0 +1,21 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Union
3
+
4
+ class DatabaseRepository(ABC):
5
+ @abstractmethod
6
+ def list_tables(self) -> List[str]:
7
+ """List all tables in the database."""
8
+ pass
9
+
10
+ @abstractmethod
11
+ def describe_table(self, table_name: str) -> str:
12
+ """Get schema information for a specific table."""
13
+ pass
14
+
15
+ @abstractmethod
16
+ def read_query(self, query: str) -> str:
17
+ """
18
+ Execute a read-only query.
19
+ Returns a formatted string (table) or an error string.
20
+ """
21
+ pass
@@ -0,0 +1,94 @@
1
+ import sqlite3
2
+ import os
3
+ from typing import List
4
+ from .repository import DatabaseRepository
5
+
6
+ class SqliteRepository(DatabaseRepository):
7
+ def __init__(self, db_path: str):
8
+ self.db_path = db_path
9
+ if not os.path.exists(self.db_path):
10
+ raise FileNotFoundError(f"Database file not found: {self.db_path}")
11
+
12
+ def _get_connection(self):
13
+ # Enforce read-only mode at driver level
14
+ return sqlite3.connect(f"file:{os.path.abspath(self.db_path)}?mode=ro", uri=True)
15
+
16
+ def list_tables(self) -> List[str]:
17
+ try:
18
+ conn = self._get_connection()
19
+ cursor = conn.cursor()
20
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
21
+ tables = [row[0] for row in cursor.fetchall()]
22
+ conn.close()
23
+ return tables
24
+ except Exception as e:
25
+ return [f"Error listing tables: {e}"]
26
+
27
+ def describe_table(self, table_name: str) -> str:
28
+ try:
29
+ conn = self._get_connection()
30
+ cursor = conn.cursor()
31
+
32
+ # Validate table existence first
33
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
34
+ tables = [row[0] for row in cursor.fetchall()]
35
+
36
+ if table_name not in tables:
37
+ conn.close()
38
+ return f"Error: Table '{table_name}' does not exist."
39
+
40
+ cursor.execute(f"PRAGMA table_info({table_name})")
41
+ columns = cursor.fetchall()
42
+ conn.close()
43
+
44
+ schema = []
45
+ for col in columns:
46
+ # PRAGMA table_info returns: cid, name, type, notnull, dflt_value, pk
47
+ col_name = col[1]
48
+ col_type = col[2]
49
+ pk = col[5]
50
+
51
+ col_str = f"- {col_name} ({col_type})"
52
+ if pk:
53
+ col_str += " [PK]"
54
+ schema.append(col_str)
55
+
56
+ return f"Schema for {table_name}:\n" + "\n".join(schema)
57
+
58
+ except Exception as e:
59
+ return f"Error describing table: {e}"
60
+
61
+ def read_query(self, query: str) -> str:
62
+ normalized_query = query.strip().upper()
63
+ if not normalized_query.startswith("SELECT"):
64
+ return "Error: Only SELECT queries are allowed."
65
+
66
+ try:
67
+ conn = self._get_connection()
68
+ cursor = conn.cursor()
69
+ cursor.execute(query)
70
+ results = cursor.fetchall()
71
+
72
+ output = []
73
+ if cursor.description:
74
+ col_names = [desc[0] for desc in cursor.description]
75
+ output.append(" | ".join(col_names))
76
+ output.append("-" * len(" | ".join(col_names)))
77
+
78
+ conn.close()
79
+
80
+ if not results:
81
+ return "No results found."
82
+
83
+ for row in results:
84
+ row_str = " | ".join(str(item) for item in row)
85
+ output.append(row_str)
86
+
87
+ return "\n".join(output)
88
+
89
+ except sqlite3.OperationalError as e:
90
+ if "readonly" in str(e):
91
+ return f"Security Error: Attempted write operation on read-only database."
92
+ return f"Database Error: {e}"
93
+ except Exception as e:
94
+ return f"Error: {e}"
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-db-server
3
+ Version: 0.1.0
4
+ Summary: MCP Database Server with read-only access
5
+ Author-email: User <user@example.com>
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: mcp
10
+ Requires-Dist: python-dotenv
11
+ Requires-Dist: pymysql
12
+ Requires-Dist: cryptography
13
+ Requires-Dist: DBUtils
14
+
15
+ # MCP Database Server
16
+
17
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server implementation in Python that provides **strictly read-only** access to databases. It allows Large Language Models (LLMs) to inspect schemas and query data safely without risking data modification.
18
+
19
+ ## Features
20
+
21
+ - **🛡️ Strict Read-Only**: Enforced at the driver level (e.g., SQLite `?mode=ro`) or session level (MySQL `SET SESSION TRANSACTION READ ONLY`).
22
+ - **🏗️ Repository Pattern**: Abstracted database access allows for easy extension to other backends (PostgreSQL, MySQL) via Dependency Injection.
23
+ - **⚡ FastMCP**: Built efficiently using the official MCP Python SDK.
24
+ - **🔌 Connection Pooling**: Automatic connection pooling for MySQL using `DBUtils` to ensure performance and reliability.
25
+ - **🔧 Easy Configuration**: Managed via environment variables and Makefiles.
26
+
27
+ ## Project Structure
28
+
29
+ - `server.py`: Main entry point for the MCP server.
30
+ - `db/`: Contains the Repository interface and implementations (`sqlite_repository.py`, `mysql_repository.py`).
31
+ - `seed.py`: Utility script to populate the database (since the server itself is read-only).
32
+ - `Makefile`: Automation for common tasks.
33
+
34
+ ## Quick Start
35
+
36
+ ### Prerequisites
37
+ - Python 3.12+ (managed via `venv`)
38
+ - `make` (optional, but recommended)
39
+ - MySQL Server (optional, if using MySQL backend)
40
+
41
+ ### 1. Setup
42
+ Use the Makefile to create a virtual environment, install dependencies, and seed a test database:
43
+
44
+ ```bash
45
+ make setup
46
+ ```
47
+
48
+ *This runs `pip install -r requirements.txt` and `python seed.py`.*
49
+
50
+ ### 2. Configuration
51
+ The project uses `python-dotenv`. Copy the example config and adjust as needed:
52
+
53
+ ```bash
54
+ cp .env.example .env
55
+ ```
56
+
57
+ **Key Variables:**
58
+ - `DB_ENGINE`: `sqlite` (Default) or `mysql`
59
+ - `DB_ADDRESS`: Path to db file (SQLite) or Host URL (MySQL).
60
+ - `DB_PORT`: Database port (Default: 3306 for MySQL).
61
+ - `DB_USER` / `DB_PASSWORD`: Database credentials (required for MySQL).
62
+ - `DB_SCHEMA`: Database name (required for MySQL).
63
+
64
+ ### 3. Run the Server
65
+ Start the MCP server:
66
+
67
+ ```bash
68
+ make run
69
+ ```
70
+ *(Or directly via `venv/bin/python server.py`)*
71
+
72
+ ## Usage with LLMs
73
+
74
+ Once running, the server exposes the following tools to connected MCP clients:
75
+
76
+ 1. **`list_tables()`**
77
+ - Returns a list of all tables in the database.
78
+ 2. **`describe_table(table_name: str)`**
79
+ - Returns the schema (columns, types, primary keys) for the specified table.
80
+ 3. **`read_query(query: str)`**
81
+ - Executes a SQL `SELECT` query.
82
+ - **Note**: Queries attempting to modify data will raise a `Security Error` or `OperationalError`.
83
+
84
+ ## Development
85
+
86
+ ### Running Tests
87
+ Execute the unit test suite:
88
+
89
+ ```bash
90
+ make test
91
+ ```
92
+
93
+ ### Extending
94
+ To add support for a new database (e.g., Postgres):
95
+ 1. Create `db/postgres_repository.py` implementation of `DatabaseRepository`.
96
+ 2. Update `server.py` to instantiate it when `DB_ENGINE=postgres`.
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ db/mysql_repository.py
4
+ db/repository.py
5
+ db/sqlite_repository.py
6
+ mcp_db_server.egg-info/PKG-INFO
7
+ mcp_db_server.egg-info/SOURCES.txt
8
+ mcp_db_server.egg-info/dependency_links.txt
9
+ mcp_db_server.egg-info/entry_points.txt
10
+ mcp_db_server.egg-info/requires.txt
11
+ mcp_db_server.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp-db-server = server:main
@@ -0,0 +1,5 @@
1
+ mcp
2
+ python-dotenv
3
+ pymysql
4
+ cryptography
5
+ DBUtils
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mcp-db-server"
7
+ version = "0.1.0"
8
+ description = "MCP Database Server with read-only access"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "User", email = "user@example.com"}
14
+ ]
15
+ dependencies = [
16
+ "mcp",
17
+ "python-dotenv",
18
+ "pymysql",
19
+ "cryptography",
20
+ "DBUtils"
21
+ ]
22
+
23
+ [project.scripts]
24
+ mcp-db-server = "server:main" # Assuming we refactor server.py to have a main() function exposed
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+