Skip to content

Support empty password when using local mysql for test #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/mysql_mcp_server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from . import server
import asyncio


def main():
"""Main entry point for the package."""
asyncio.run(server.main())
"""Main entry point for the package."""
asyncio.run(server.main())


# Expose important items at package level
__all__ = ['main', 'server']
__all__ = ['main', 'server']
53 changes: 33 additions & 20 deletions src/mysql_mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
)
logger = logging.getLogger("mysql_mcp_server")


def get_db_config():
"""Get database configuration from environment variables."""
config = {
Expand All @@ -22,17 +23,22 @@ def get_db_config():
"password": os.getenv("MYSQL_PASSWORD"),
"database": os.getenv("MYSQL_DATABASE")
}

if not all([config["user"], config["password"], config["database"]]):

if not config["password"]:
logger.warning("No password provided. Using empty password.")

if not all([config["user"], config["database"]]):
logger.error("Missing required database configuration. Please check environment variables:")
logger.error("MYSQL_USER, MYSQL_PASSWORD, and MYSQL_DATABASE are required")
logger.error("MYSQL_USER and MYSQL_DATABASE are required")
raise ValueError("Missing required database configuration")

return config


# Initialize server
app = Server("mysql_mcp_server")


@app.list_resources()
async def list_resources() -> list[Resource]:
"""List MySQL tables as resources."""
Expand All @@ -43,7 +49,7 @@ async def list_resources() -> list[Resource]:
cursor.execute("SHOW TABLES")
tables = cursor.fetchall()
logger.info(f"Found tables: {tables}")

resources = []
for table in tables:
resources.append(
Expand All @@ -59,19 +65,20 @@ async def list_resources() -> list[Resource]:
logger.error(f"Failed to list resources: {str(e)}")
return []


@app.read_resource()
async def read_resource(uri: AnyUrl) -> str:
"""Read table contents."""
config = get_db_config()
uri_str = str(uri)
logger.info(f"Reading resource: {uri_str}")

if not uri_str.startswith("mysql://"):
raise ValueError(f"Invalid URI scheme: {uri_str}")

parts = uri_str[8:].split('/')
table = parts[0]

try:
with connect(**config) as conn:
with conn.cursor() as cursor:
Expand All @@ -80,11 +87,12 @@ async def read_resource(uri: AnyUrl) -> str:
rows = cursor.fetchall()
result = [",".join(map(str, row)) for row in rows]
return "\n".join([",".join(columns)] + result)

except Error as e:
logger.error(f"Database error reading resource {uri}: {str(e)}")
raise RuntimeError(f"Database error: {str(e)}")


@app.list_tools()
async def list_tools() -> list[Tool]:
"""List available MySQL tools."""
Expand All @@ -106,55 +114,59 @@ async def list_tools() -> list[Tool]:
)
]


@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Execute SQL commands."""
config = get_db_config()
logger.info(f"Calling tool: {name} with arguments: {arguments}")

if name != "execute_sql":
raise ValueError(f"Unknown tool: {name}")

query = arguments.get("query")
if not query:
raise ValueError("Query is required")

try:
with connect(**config) as conn:
with conn.cursor() as cursor:
cursor.execute(query)

# Special handling for SHOW TABLES
if query.strip().upper().startswith("SHOW TABLES"):
tables = cursor.fetchall()
result = ["Tables_in_" + config["database"]] # Header
result.extend([table[0] for table in tables])
return [TextContent(type="text", text="\n".join(result))]

# Regular SELECT queries
elif query.strip().upper().startswith("SELECT"):
columns = [desc[0] for desc in cursor.description]
rows = cursor.fetchall()
result = [",".join(map(str, row)) for row in rows]
return [TextContent(type="text", text="\n".join([",".join(columns)] + result))]

# Non-SELECT queries
else:
conn.commit()
return [TextContent(type="text", text=f"Query executed successfully. Rows affected: {cursor.rowcount}")]

return [TextContent(
type="text", text=f"Query executed successfully. Rows affected: {cursor.rowcount}"
)]

except Error as e:
logger.error(f"Error executing SQL '{query}': {e}")
return [TextContent(type="text", text=f"Error executing query: {str(e)}")]


async def main():
"""Main entry point to run the MCP server."""
from mcp.server.stdio import stdio_server

logger.info("Starting MySQL MCP server...")
config = get_db_config()
logger.info(f"Database config: {config['host']}/{config['database']} as {config['user']}")

async with stdio_server() as (read_stream, write_stream):
try:
await app.run(
Expand All @@ -166,5 +178,6 @@ async def main():
logger.error(f"Server error: {str(e)}", exc_info=True)
raise


if __name__ == "__main__":
asyncio.run(main())
asyncio.run(main())
12 changes: 7 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import mysql.connector
from mysql.connector import Error


@pytest.fixture(scope="session")
def mysql_connection():
"""Create a test database connection."""
Expand All @@ -14,7 +15,7 @@ def mysql_connection():
password=os.getenv("MYSQL_PASSWORD", "testpassword"),
database=os.getenv("MYSQL_DATABASE", "test_db")
)

if connection.is_connected():
# Create a test table
cursor = connection.cursor()
Expand All @@ -26,21 +27,22 @@ def mysql_connection():
)
""")
connection.commit()

yield connection

# Cleanup
cursor.execute("DROP TABLE IF EXISTS test_table")
connection.commit()
cursor.close()
connection.close()

except Error as e:
pytest.fail(f"Failed to connect to MySQL: {e}")


@pytest.fixture(scope="session")
def mysql_cursor(mysql_connection):
"""Create a test cursor."""
cursor = mysql_connection.cursor()
yield cursor
cursor.close()
cursor.close()
7 changes: 6 additions & 1 deletion tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from mysql_mcp_server.server import app, list_tools, list_resources, read_resource, call_tool
from pydantic import AnyUrl


def test_server_initialization():
"""Test that the server initializes correctly."""
assert app.name == "mysql_mcp_server"


@pytest.mark.asyncio
async def test_list_tools():
"""Test that list_tools returns expected tools."""
Expand All @@ -14,18 +16,21 @@ async def test_list_tools():
assert tools[0].name == "execute_sql"
assert "query" in tools[0].inputSchema["properties"]


@pytest.mark.asyncio
async def test_call_tool_invalid_name():
"""Test calling a tool with an invalid name."""
with pytest.raises(ValueError, match="Unknown tool"):
await call_tool("invalid_tool", {})


@pytest.mark.asyncio
async def test_call_tool_missing_query():
"""Test calling execute_sql without a query."""
with pytest.raises(ValueError, match="Query is required"):
await call_tool("execute_sql", {})


# Skip database-dependent tests if no database connection
@pytest.mark.asyncio
@pytest.mark.skipif(
Expand All @@ -43,4 +48,4 @@ async def test_list_resources():
except ValueError as e:
if "Missing required database configuration" in str(e):
pytest.skip("Database configuration not available")
raise
raise