#
tokens: 4720/50000 9/9 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | 
```