# Directory Structure ``` ├── .env.sample ├── .gitignore ├── .python-version ├── LICENSE ├── pyproject.toml ├── README.md ├── src │ └── mcp_server_mariadb │ ├── __init__.py │ └── server.py ├── tests │ ├── __init__.py │ └── test_db.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 1 | 3.10 2 | ``` -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- ``` 1 | MARIADB_HOST=localhost 2 | MARIADB_USER=test_user 3 | MARIADB_PASSWORD=test_password 4 | MARIADB_DATABASE=test_db 5 | MARIADB_PORT=3306 6 | 7 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # mcp-server-mariadb 2 | 3 | An MCP server implementation for retrieving data from mariadb 4 | 5 | ## Features 6 | 7 | ### Resources 8 | 9 | Expose schema list in database 10 | 11 | ### Tools 12 | 13 | - query_database 14 | - Execute read-only operations against MariDB 15 | 16 | ## dependency 17 | 18 | ### install mariadb 19 | 20 | - mac 21 | - when install mariadb, 22 | maybe raise os error below. 23 | you can resolve by installing mariadb-connector-c. 24 | 25 | ```bash 26 | 27 | OSError: mariadb_config not found. 28 | 29 | This error typically indicates that MariaDB Connector/C, a dependency which 30 | must be preinstalled, is not found. 31 | If MariaDB Connector/C is not installed, see installation instructions 32 | If MariaDB Connector/C is installed, either set the environment variable 33 | MARIADB_CONFIG or edit the configuration file 'site.cfg' to set the 34 | 'mariadb_config' option to the file location of the mariadb_config utility. 35 | 36 | 37 | ``` 38 | 39 | 1. execute `brew install mariadb-connector-c` 40 | 2. execute `echo 'export PATH="/opt/homebrew/opt/mariadb-connector-c/bin:$PATH"' >> ~/.bashrc` 41 | 3. set environment variable `export MARIADB_CONFIG=$(brew --prefix mariadb-connector-c)/bin/mariadb_config` 42 | 4. execute `uv add mariadb` again. 43 | 44 | ## Usage with Claude Desktop 45 | 46 | ### Configuration File 47 | 48 | Paths to Claude Desktop config file: 49 | 50 | - **MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 51 | - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` 52 | <!-- markdownlint-disable MD033 --> 53 | <details> 54 | <summary>Add this configuration to enable published servers</summary> 55 | 56 | ```json 57 | 58 | { 59 | "mcpServers": { 60 | "mcp_server_mariadb": { 61 | "command": "/PATH/TO/uvx" 62 | "args": [ 63 | "mcp-server-mariadb", 64 | "--host", 65 | "${DB_HOST}", 66 | "--port", 67 | "${DB_PORT}", 68 | "--user", 69 | "${DB_USER}", 70 | "--password", 71 | "${DB_PASSWORD}", 72 | "--database", 73 | "${DB_NAME}" 74 | ] 75 | } 76 | } 77 | } 78 | 79 | ``` 80 | 81 | **Note**: Replace these placeholders with actual paths: 82 | 83 | - `/PATH/TO/uvx`: Full path to uvx executable 84 | 85 | </details> 86 | 87 | <details> 88 | <summary>Add this configuration to enable development/unpublished servers</summary> 89 | 90 | ```json 91 | { 92 | "mcpServers": { 93 | "mcp_server_mariadb": { 94 | "command": "/PATH/TO/uv", 95 | "args": [ 96 | "--directory", 97 | "/YOUR/SOURCE/PATH/mcp-server-mariadb/src/mcp_server_mariadb", 98 | "run", 99 | "server.py" 100 | ], 101 | "env": { 102 | "MARIADB_HOST": "127.0.0.1", 103 | "MARIADB_USER": "USER", 104 | "MARIADB_PASSWORD": "PASSWORD", 105 | "MARIADB_DATABASE": "DATABASE", 106 | "MARIADB_PORT": "3306" 107 | } 108 | } 109 | } 110 | } 111 | ``` 112 | 113 | **Note**: Replace these placeholders with actual paths: 114 | 115 | - `/PATH/TO/uv`: Full path to UV executable 116 | - `/YOUR/SOURCE/PATH/mcp-server-mariadb/src/mcp_server_mariadb`: Path to server source code 117 | 118 | </details> 119 | 120 | ## License 121 | 122 | This mcp server is licensed under the MIT license. please see the LICENSE file in the repository. 123 | ``` -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- ```python 1 | ``` -------------------------------------------------------------------------------- /src/mcp_server_mariadb/__init__.py: -------------------------------------------------------------------------------- ```python 1 | from . import server 2 | 3 | 4 | def main(): 5 | """Entry point for the MCP server package.""" 6 | server.main() 7 | 8 | 9 | __all__ = ["main"] 10 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "mcp-server-mariadb" 3 | version = "0.1.2" 4 | description = "MCP Server for MariaDB" 5 | readme = "README.md" 6 | authors = [ 7 | { name = "HeejunShin", email = "[email protected]" } 8 | ] 9 | requires-python = ">=3.10" 10 | dependencies = [ 11 | "mariadb>=1.1.12", 12 | "mcp[cli]>=1.3.0", 13 | "mysql-connector-python>=9.2.0", 14 | "pytest>=8.3.4", 15 | "python-dotenv>=1.0.1", 16 | ] 17 | 18 | [project.scripts] 19 | mcp-server-mariadb = "mcp_server_mariadb:main" 20 | 21 | [build-system] 22 | requires = ["hatchling"] 23 | build-backend = "hatchling.build" 24 | ``` -------------------------------------------------------------------------------- /tests/test_db.py: -------------------------------------------------------------------------------- ```python 1 | from dotenv import load_dotenv 2 | from src.mcp_server_mariadb.server import get_connection, is_read_only_query 3 | 4 | load_dotenv() 5 | 6 | 7 | def test_db_connection(): 8 | connection = get_connection() 9 | 10 | assert connection is not None 11 | 12 | 13 | def test_is_read_only_query(): 14 | assert is_read_only_query("SELECT * FROM users") 15 | assert is_read_only_query("SHOW TABLES") 16 | assert is_read_only_query("DESCRIBE users") 17 | assert is_read_only_query("DESC users") 18 | assert is_read_only_query("EXPLAIN SELECT * FROM users") 19 | 20 | 21 | def test_is_not_read_only_query(): 22 | assert not is_read_only_query( 23 | "INSERT INTO users (name, email) VALUES ('John', '[email protected]')" 24 | ) 25 | assert not is_read_only_query( 26 | "UPDATE users SET email = '[email protected]' WHERE id = 1" 27 | ) 28 | assert not is_read_only_query("DELETE FROM users WHERE id = 1") 29 | assert not is_read_only_query( 30 | "CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255))" 31 | ) 32 | assert not is_read_only_query("DROP TABLE users") 33 | ``` -------------------------------------------------------------------------------- /src/mcp_server_mariadb/server.py: -------------------------------------------------------------------------------- ```python 1 | import argparse 2 | import os 3 | from contextlib import closing 4 | from dataclasses import dataclass, field 5 | 6 | import mariadb 7 | from dotenv import load_dotenv 8 | from mcp.server.fastmcp import FastMCP 9 | 10 | load_dotenv() 11 | 12 | mcp = FastMCP( 13 | "MariaDB Explorer", dependencies=["mysql-connector-python", "python-dotenv"] 14 | ) 15 | 16 | READ_ONLY_KEYWORDS = ("SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN") 17 | READ_ONLY_KEYWORD_NAMES = ", ".join(READ_ONLY_KEYWORDS) 18 | 19 | 20 | def get_arguments() -> dict: 21 | """Parse command-line arguments and return as a dictionary.""" 22 | parser = argparse.ArgumentParser(description="MariaDB Configuration") 23 | parser.add_argument("--host", help="MariaDB host") 24 | parser.add_argument("--port", type=int, help="MariaDB port") 25 | parser.add_argument("--user", help="MariaDB user") 26 | parser.add_argument("--password", help="MariaDB password") 27 | parser.add_argument("--database", help="MariaDB database") 28 | args = parser.parse_args() 29 | 30 | return {k: v for k, v in vars(args).items() if v is not None} 31 | 32 | 33 | @dataclass 34 | class DBconfig: 35 | """Database configuration""" 36 | 37 | host: str = field(default_factory=lambda: os.getenv("MARIADB_HOST", "localhost")) 38 | port: int = field(default_factory=lambda: int(os.getenv("MARIADB_PORT", "3306"))) 39 | user: str = field(default_factory=lambda: os.getenv("MARIADB_USER", "")) 40 | password: str = field(default_factory=lambda: os.getenv("MARIADB_PASSWORD", "")) 41 | database: str = field(default_factory=lambda: os.getenv("MARIADB_DATABASE", "")) 42 | 43 | @classmethod 44 | def from_args(cls) -> "DBconfig": 45 | """Create a DBconfig instance from command-line arguments and environment variables.""" 46 | cli_args = get_arguments() 47 | return cls(**{**cls().__dict__, **cli_args}) 48 | 49 | 50 | def get_connection(): 51 | """Create a connection to the database connection""" 52 | config = DBconfig.from_args() 53 | 54 | try: 55 | conn = mariadb.connect( 56 | user=config.user, 57 | password=config.password, 58 | host=config.host, 59 | port=config.port, 60 | database=config.database, 61 | ) 62 | return conn 63 | except mariadb.Error as e: 64 | print(f"Error connecting to MariaDB Platform: {e}") 65 | 66 | 67 | def is_read_only_query(query: str) -> bool: 68 | """check if a query is read-only by examining its first word""" 69 | first_word = query.strip().split()[0].upper() 70 | 71 | return first_word in READ_ONLY_KEYWORDS 72 | 73 | 74 | @mcp.resource("schema://tables") 75 | def list_tables() -> str: 76 | """Get the schema for a specific table""" 77 | try: 78 | with closing(get_connection()) as conn: 79 | cursor = conn.cursor() 80 | cursor.execute("SHOW TABLES") 81 | tables = cursor.fetchall() 82 | return "\n".join(table[0] for table in tables) 83 | except Exception as e: 84 | return f"Error retrieving tables: {str(e)}" 85 | 86 | 87 | @mcp.tool() 88 | def query_database(query: str) -> str: 89 | """ 90 | Execute a read-only SQL query on the database 91 | 92 | Args: 93 | query: SQL query to execute (must be SELECT, SHOW, DESCRIBE, DESC, EXPLAIN) 94 | """ 95 | 96 | if not is_read_only_query(query): 97 | return "Error: Only read-only queries (SELECT, SHOW, DESCRIBE, DESC, EXPLAIN) are allowed" 98 | 99 | try: 100 | with closing(get_connection()) as conn: 101 | cursor = conn.cursor() 102 | cursor.execute(query) 103 | results = cursor.fetchall() 104 | 105 | # Get column names 106 | columns = [desc[0] for desc in cursor.description] 107 | 108 | # Format results as a table 109 | output = [] 110 | output.append(" | ".join(columns)) 111 | output.append( 112 | "-" * (sum(len(col) for col in columns) + 3 * (len(columns) - 1)) 113 | ) 114 | 115 | for row in results: 116 | output.append(" | ".join(str(val) for val in row)) 117 | 118 | return "\n".join(output) 119 | except Exception as e: 120 | return f"Error executing query {str(e)}" 121 | 122 | 123 | def main(): 124 | mcp.run(transport="stdio") 125 | 126 | 127 | if __name__ == "__main__": 128 | main() 129 | ```