diff --git a/MssqlMcp/python/.gitignore b/MssqlMcp/python/.gitignore new file mode 100644 index 0000000..c6f0793 --- /dev/null +++ b/MssqlMcp/python/.gitignore @@ -0,0 +1,140 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/MssqlMcp/python/LICENSE b/MssqlMcp/python/LICENSE new file mode 100644 index 0000000..48ea661 --- /dev/null +++ b/MssqlMcp/python/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE diff --git a/MssqlMcp/python/README.md b/MssqlMcp/python/README.md new file mode 100644 index 0000000..678a3ec --- /dev/null +++ b/MssqlMcp/python/README.md @@ -0,0 +1,267 @@ +# MSSQL MCP Server - Python Implementation + +A Python implementation of the Model Context Protocol (MCP) server for Microsoft SQL Server database operations. This server provides tools for database management including table operations, data manipulation, and schema inspection. + +## Features + +This MCP server provides the following tools: + +- **List Tables**: Lists all tables in the SQL Database +- **Describe Table**: Returns detailed table schema information including columns, indexes, constraints, and foreign keys +- **Create Table**: Creates new tables using CREATE TABLE SQL statements +- **Drop Table**: Drops tables using DROP TABLE SQL statements +- **Read Data**: Executes SELECT queries to read data from the database +- **Insert Data**: Inserts data using INSERT SQL statements +- **Update Data**: Updates data using UPDATE SQL statements + +## Prerequisites + +- Python 3.8 or higher +- Microsoft SQL Server (local or remote) +- ODBC Driver for SQL Server + +### Installing ODBC Driver for SQL Server + +#### Windows +The ODBC driver is typically pre-installed on Windows systems. + +#### macOS +```bash +# Install using Homebrew +brew install microsoft/mssql-release/msodbcsql18 microsoft/mssql-release/mssql-tools18 + +# Or download from Microsoft's website +# https://docs.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server +``` + +#### Linux (Ubuntu/Debian) +```bash +# Import Microsoft repository key +curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - + +# Add Microsoft repository +curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | sudo tee /etc/apt/sources.list.d/msprod.list + +# Update package list +sudo apt-get update + +# Install ODBC driver +sudo apt-get install msodbcsql18 +``` + +## Installation + +1. Clone or download this repository +2. Navigate to the Python implementation directory: + ```bash + cd MssqlMcp/python + ``` + +3. Create a virtual environment (recommended): + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +4. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +## Configuration + +Set the connection string environment variable: + +### Windows +```cmd +set CONNECTION_STRING=Server=.;Database=test;Trusted_Connection=yes;TrustServerCertificate=yes +``` + +### macOS/Linux +```bash +export CONNECTION_STRING="Server=.;Database=test;Trusted_Connection=yes;TrustServerCertificate=yes" +``` + +### Connection String Examples + +#### Local SQL Server with Windows Authentication +``` +Server=.;Database=test;Trusted_Connection=yes;TrustServerCertificate=yes +``` + +#### Local SQL Server with SQL Authentication +``` +Server=.;Database=test;User Id=myuser;Password=mypassword;TrustServerCertificate=yes +``` + +#### Remote SQL Server +``` +Server=myserver.database.windows.net;Database=mydatabase;User Id=myuser;Password=mypassword;Encrypt=yes;TrustServerCertificate=no +``` + +#### Azure SQL Database +``` +Server=tcp:myserver.database.windows.net,1433;Database=mydatabase;User Id=myuser@myserver;Password=mypassword;Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30; +``` + +## Usage + +### Running the Server + +```bash +python main.py +``` + +The server will start and listen for MCP protocol messages via standard input/output. + +### Using with MCP Clients + +This server is designed to work with MCP-compatible clients. The server provides the following tools: + +#### List Tables +```json +{ + "name": "list_tables", + "arguments": {} +} +``` + +#### Describe Table +```json +{ + "name": "describe_table", + "arguments": { + "table_name": "dbo.Users" + } +} +``` + +#### Create Table +```json +{ + "name": "create_table", + "arguments": { + "sql": "CREATE TABLE dbo.Users (Id INT PRIMARY KEY, Name NVARCHAR(100))" + } +} +``` + +#### Drop Table +```json +{ + "name": "drop_table", + "arguments": { + "sql": "DROP TABLE dbo.Users" + } +} +``` + +#### Read Data +```json +{ + "name": "read_data", + "arguments": { + "sql": "SELECT * FROM dbo.Users WHERE Id = 1" + } +} +``` + +#### Insert Data +```json +{ + "name": "insert_data", + "arguments": { + "sql": "INSERT INTO dbo.Users (Id, Name) VALUES (1, 'John Doe')" + } +} +``` + +#### Update Data +```json +{ + "name": "update_data", + "arguments": { + "sql": "UPDATE dbo.Users SET Name = 'Jane Doe' WHERE Id = 1" + } +} +``` + +## Project Structure + +``` +python/ +├── main.py # Entry point script +├── requirements.txt # Python dependencies +├── README.md # This file +└── src/ + └── mssql_mcp/ + ├── __init__.py + ├── server.py # Main MCP server implementation + ├── db_operation_result.py # Result class for database operations + ├── sql_connection_factory.py # Database connection management + └── tools/ # Database operation tools + ├── __init__.py + ├── list_tables.py + ├── describe_table.py + ├── create_table.py + ├── drop_table.py + ├── read_data.py + ├── insert_data.py + └── update_data.py +``` + +## Error Handling + +The server provides comprehensive error handling: + +- Connection errors are logged and returned to the client +- SQL execution errors are captured and returned with details +- Invalid tool requests are handled gracefully +- All database connections are properly closed after use + +## Security Considerations + +- Always use parameterized queries when possible (implemented in describe_table) +- Validate SQL statements before execution +- Use appropriate authentication methods for your environment +- Consider using read-only connections for query operations +- Regularly update dependencies for security patches + +## Troubleshooting + +### Common Issues + +1. **Connection String Not Set** + - Ensure the `CONNECTION_STRING` environment variable is properly set + - Verify the connection string format is correct + +2. **ODBC Driver Not Found** + - Install the Microsoft ODBC Driver for SQL Server + - Verify the driver is properly installed and accessible + +3. **Authentication Failures** + - Check username/password for SQL authentication + - Verify Windows authentication settings + - Ensure the user has appropriate database permissions + +4. **SSL/TLS Certificate Issues** + - Use `TrustServerCertificate=yes` for development environments + - Ensure proper certificate configuration for production + +### Logging + +The server logs important events and errors to stderr. Check the logs for detailed error information when troubleshooting issues. + +## Contributing + +This is a Microsoft-provided sample implementation. For issues or contributions, please refer to the main repository guidelines. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Related Projects + +- [.NET Implementation](../dotnet/) - C# implementation of the same MCP server +- [Node.js Implementation](../Node/) - TypeScript/JavaScript implementation +- [Model Context Protocol](https://github.com/modelcontextprotocol) - Official MCP specification and tools diff --git a/MssqlMcp/python/example.py b/MssqlMcp/python/example.py new file mode 100755 index 0000000..9ba63b2 --- /dev/null +++ b/MssqlMcp/python/example.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +Example script demonstrating how to use the MSSQL MCP Server tools directly. +This is for testing and development purposes. +""" + +import asyncio +import os +import sys +import logging + +# Add the src directory to the Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from src.mssql_mcp.sql_connection_factory import SqlConnectionFactory +from src.mssql_mcp.tools import ( + ListTablesTool, + DescribeTableTool, + CreateTableTool, + ReadDataTool, + InsertDataTool, + UpdateDataTool, + DropTableTool +) + + +async def main(): + """Example usage of the MSSQL MCP tools.""" + + # Set up logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + + # Check if connection string is set + if not os.getenv("CONNECTION_STRING"): + print("Please set the CONNECTION_STRING environment variable") + print("Example: export CONNECTION_STRING='Server=.;Database=test;Trusted_Connection=yes;TrustServerCertificate=yes'") + return + + try: + # Initialize connection factory and tools + connection_factory = SqlConnectionFactory() + + # Initialize tools + list_tables = ListTablesTool(connection_factory, logger) + describe_table = DescribeTableTool(connection_factory, logger) + create_table = CreateTableTool(connection_factory, logger) + read_data = ReadDataTool(connection_factory, logger) + insert_data = InsertDataTool(connection_factory, logger) + update_data = UpdateDataTool(connection_factory, logger) + drop_table = DropTableTool(connection_factory, logger) + + print("=== MSSQL MCP Server Tools Example ===\n") + + # Example 1: List all tables + print("1. Listing all tables:") + result = await list_tables.execute() + if result.success: + print(f" Found {len(result.data)} tables:") + for table in result.data: + print(f" - {table}") + else: + print(f" Error: {result.error}") + print() + + # Example 2: Create a test table + print("2. Creating a test table:") + create_sql = """ + CREATE TABLE dbo.TestUsers ( + Id INT PRIMARY KEY IDENTITY(1,1), + Name NVARCHAR(100) NOT NULL, + Email NVARCHAR(255) UNIQUE, + CreatedDate DATETIME2 DEFAULT GETDATE() + ) + """ + result = await create_table.execute(create_sql) + if result.success: + print(" Test table created successfully") + else: + print(f" Error: {result.error}") + print() + + # Example 3: Describe the test table + print("3. Describing the test table:") + result = await describe_table.execute("dbo.TestUsers") + if result.success: + table_info = result.data + print(f" Table: {table_info['table']['name']}") + print(f" Schema: {table_info['table']['schema']}") + print(f" Columns:") + for col in table_info['columns']: + nullable = "NULL" if col['nullable'] else "NOT NULL" + print(f" - {col['name']}: {col['type']} {nullable}") + else: + print(f" Error: {result.error}") + print() + + # Example 4: Insert some test data + print("4. Inserting test data:") + insert_sql = """ + INSERT INTO dbo.TestUsers (Name, Email) VALUES + ('John Doe', 'john.doe@example.com'), + ('Jane Smith', 'jane.smith@example.com'), + ('Bob Johnson', 'bob.johnson@example.com') + """ + result = await insert_data.execute(insert_sql) + if result.success: + print(f" Inserted {result.rows_affected} rows") + else: + print(f" Error: {result.error}") + print() + + # Example 5: Read the data + print("5. Reading test data:") + read_sql = "SELECT * FROM dbo.TestUsers ORDER BY Id" + result = await read_data.execute(read_sql) + if result.success: + print(f" Found {len(result.data)} rows:") + for row in result.data: + print(f" ID: {row['Id']}, Name: {row['Name']}, Email: {row['Email']}") + else: + print(f" Error: {result.error}") + print() + + # Example 6: Update some data + print("6. Updating test data:") + update_sql = "UPDATE dbo.TestUsers SET Name = 'John Updated' WHERE Id = 1" + result = await update_data.execute(update_sql) + if result.success: + print(f" Updated {result.rows_affected} rows") + else: + print(f" Error: {result.error}") + print() + + # Example 7: Read updated data + print("7. Reading updated data:") + read_sql = "SELECT * FROM dbo.TestUsers WHERE Id = 1" + result = await read_data.execute(read_sql) + if result.success: + if result.data: + row = result.data[0] + print(f" Updated row: ID: {row['Id']}, Name: {row['Name']}, Email: {row['Email']}") + else: + print(" No data found") + else: + print(f" Error: {result.error}") + print() + + # Example 8: Clean up - drop the test table + print("8. Cleaning up - dropping test table:") + drop_sql = "DROP TABLE dbo.TestUsers" + result = await drop_table.execute(drop_sql) + if result.success: + print(" Test table dropped successfully") + else: + print(f" Error: {result.error}") + print() + + print("=== Example completed successfully ===") + + except Exception as ex: + print(f"Example failed with error: {str(ex)}") + logger.exception("Example execution failed") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/MssqlMcp/python/main.py b/MssqlMcp/python/main.py new file mode 100755 index 0000000..911ba54 --- /dev/null +++ b/MssqlMcp/python/main.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +Entry point for the MSSQL MCP Server. +""" + +import asyncio +import sys +import os + +# Add the src directory to the Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from src.mssql_mcp.server import main + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/MssqlMcp/python/requirements.txt b/MssqlMcp/python/requirements.txt new file mode 100644 index 0000000..6b5527f --- /dev/null +++ b/MssqlMcp/python/requirements.txt @@ -0,0 +1,13 @@ +# Core dependencies +pyodbc>=5.0.1 +mcp>=1.0.0 + +# Optional dependencies for enhanced functionality +python-dotenv>=1.0.0 +asyncio-mqtt>=0.16.1 + +# Development dependencies (optional) +pytest>=7.0.0 +pytest-asyncio>=0.21.0 +black>=23.0.0 +flake8>=6.0.0 diff --git a/MssqlMcp/python/setup.py b/MssqlMcp/python/setup.py new file mode 100644 index 0000000..d3a4d78 --- /dev/null +++ b/MssqlMcp/python/setup.py @@ -0,0 +1,65 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +Setup script for MSSQL MCP Server Python implementation. +""" + +from setuptools import setup, find_packages +import os + +# Read the README file +def read_readme(): + readme_path = os.path.join(os.path.dirname(__file__), 'README.md') + if os.path.exists(readme_path): + with open(readme_path, 'r', encoding='utf-8') as f: + return f.read() + return "" + +# Read requirements +def read_requirements(): + requirements_path = os.path.join(os.path.dirname(__file__), 'requirements.txt') + if os.path.exists(requirements_path): + with open(requirements_path, 'r', encoding='utf-8') as f: + return [line.strip() for line in f if line.strip() and not line.startswith('#')] + return [] + +setup( + name="mssql-mcp-server", + version="1.0.0", + description="A Python implementation of the Model Context Protocol (MCP) server for Microsoft SQL Server database operations", + long_description=read_readme(), + long_description_content_type="text/markdown", + author="Microsoft Corporation", + author_email="", + url="https://github.com/microsoft/SQL-AI-samples", + packages=find_packages(where="src"), + package_dir={"": "src"}, + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Database", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + python_requires=">=3.8", + install_requires=read_requirements(), + entry_points={ + "console_scripts": [ + "mssql-mcp-server=mssql_mcp.server:main", + ], + }, + keywords="mcp model-context-protocol sql-server database mssql", + project_urls={ + "Bug Reports": "https://github.com/microsoft/SQL-AI-samples/issues", + "Source": "https://github.com/microsoft/SQL-AI-samples", + "Documentation": "https://github.com/microsoft/SQL-AI-samples/tree/main/MssqlMcp/python", + }, +) diff --git a/MssqlMcp/python/src/mssql_mcp/__init__.py b/MssqlMcp/python/src/mssql_mcp/__init__.py new file mode 100644 index 0000000..ea70fa6 --- /dev/null +++ b/MssqlMcp/python/src/mssql_mcp/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +MSSQL MCP Server - Python implementation + +A Model Context Protocol (MCP) server for Microsoft SQL Server database operations. +Provides tools for database management including table operations, data manipulation, +and schema inspection. +""" + +__version__ = "1.0.0" +__author__ = "Microsoft Corporation" diff --git a/MssqlMcp/python/src/mssql_mcp/db_operation_result.py b/MssqlMcp/python/src/mssql_mcp/db_operation_result.py new file mode 100644 index 0000000..b664c0e --- /dev/null +++ b/MssqlMcp/python/src/mssql_mcp/db_operation_result.py @@ -0,0 +1,66 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +Database operation result class for consistent return values across all tools. +""" + +from typing import Any, Optional + + +class DbOperationResult: + """ + Represents the result of a database operation, including success status, + error message, number of rows affected, and any returned data. + """ + + def __init__( + self, + success: bool, + error: Optional[str] = None, + rows_affected: Optional[int] = None, + data: Optional[Any] = None + ): + """ + Initialize a database operation result. + + Args: + success: Whether the database operation was successful + error: Error message if the operation failed; otherwise None + rows_affected: Number of rows affected by the operation, if applicable + data: Any data returned by the operation, such as query results + """ + self.success = success + self.error = error + self.rows_affected = rows_affected + self.data = data + + def to_dict(self) -> dict: + """ + Convert the result to a dictionary for JSON serialization. + + Returns: + Dictionary representation of the result + """ + result = { + "success": self.success + } + + if self.error is not None: + result["error"] = self.error + + if self.rows_affected is not None: + result["rows_affected"] = self.rows_affected + + if self.data is not None: + result["data"] = self.data + + return result + + def __repr__(self) -> str: + """String representation of the result.""" + return ( + f"DbOperationResult(success={self.success}, " + f"error={self.error}, rows_affected={self.rows_affected}, " + f"data={self.data})" + ) diff --git a/MssqlMcp/python/src/mssql_mcp/server.py b/MssqlMcp/python/src/mssql_mcp/server.py new file mode 100644 index 0000000..870a166 --- /dev/null +++ b/MssqlMcp/python/src/mssql_mcp/server.py @@ -0,0 +1,208 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +MSSQL MCP Server implementation. +""" + +import asyncio +import json +import logging +import sys +from typing import Dict, Any, List, Optional + +from mcp.server import Server +from mcp.server.stdio import stdio_server +from mcp.types import ( + CallToolRequest, + CallToolResult, + ListToolsRequest, + ListToolsResult, + Tool, + TextContent, + ImageContent, + EmbeddedResource +) + +from .sql_connection_factory import SqlConnectionFactory +from .tools import ( + ListTablesTool, + DescribeTableTool, + CreateTableTool, + DropTableTool, + ReadDataTool, + InsertDataTool, + UpdateDataTool +) + + +class MssqlMcpServer: + """ + MSSQL MCP Server that provides database tools via the Model Context Protocol. + """ + + def __init__(self): + """Initialize the MSSQL MCP Server.""" + self.logger = self._setup_logging() + self.connection_factory = SqlConnectionFactory() + self.server = Server("mssql-mcp-server") + self.tools = self._initialize_tools() + self._setup_handlers() + + def _setup_logging(self) -> logging.Logger: + """Set up logging configuration.""" + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stderr) + ] + ) + return logging.getLogger(__name__) + + def _initialize_tools(self) -> Dict[str, Any]: + """Initialize all available tools.""" + return { + "list_tables": ListTablesTool(self.connection_factory, self.logger), + "describe_table": DescribeTableTool(self.connection_factory, self.logger), + "create_table": CreateTableTool(self.connection_factory, self.logger), + "drop_table": DropTableTool(self.connection_factory, self.logger), + "read_data": ReadDataTool(self.connection_factory, self.logger), + "insert_data": InsertDataTool(self.connection_factory, self.logger), + "update_data": UpdateDataTool(self.connection_factory, self.logger) + } + + def _setup_handlers(self): + """Set up MCP request handlers.""" + self.server.list_tools = self._handle_list_tools + self.server.call_tool = self._handle_call_tool + + async def _handle_list_tools(self, request: ListToolsRequest) -> ListToolsResult: + """ + Handle list tools request. + + Args: + request: List tools request + + Returns: + List of available tools + """ + tools = [] + + for tool_name, tool_instance in self.tools.items(): + tool = Tool( + name=tool_instance.name, + description=tool_instance.description, + inputSchema={ + "type": "object", + "properties": self._get_tool_properties(tool_name), + "required": self._get_required_properties(tool_name) + } + ) + tools.append(tool) + + return ListToolsResult(tools=tools) + + def _get_tool_properties(self, tool_name: str) -> Dict[str, Any]: + """Get input schema properties for a tool.""" + if tool_name == "describe_table": + return { + "table_name": { + "type": "string", + "description": "Name of table" + } + } + elif tool_name in ["create_table", "drop_table", "read_data", "insert_data", "update_data"]: + return { + "sql": { + "type": "string", + "description": "SQL statement to execute" + } + } + else: + return {} + + def _get_required_properties(self, tool_name: str) -> List[str]: + """Get required properties for a tool.""" + if tool_name == "describe_table": + return ["table_name"] + elif tool_name in ["create_table", "drop_table", "read_data", "insert_data", "update_data"]: + return ["sql"] + else: + return [] + + async def _handle_call_tool(self, request: CallToolRequest) -> CallToolResult: + """ + Handle call tool request. + + Args: + request: Call tool request + + Returns: + Tool execution result + """ + tool_name = request.params.name + arguments = request.params.arguments or {} + + try: + if tool_name not in self.tools: + return CallToolResult( + content=[TextContent(type="text", text=f"Unknown tool: {tool_name}")], + isError=True + ) + + tool_instance = self.tools[tool_name] + result = await self._execute_tool(tool_instance, arguments) + + if result.success: + content_text = json.dumps(result.to_dict(), indent=2) + return CallToolResult( + content=[TextContent(type="text", text=content_text)] + ) + else: + return CallToolResult( + content=[TextContent(type="text", text=f"Error: {result.error}")], + isError=True + ) + + except Exception as ex: + self.logger.error(f"Tool execution failed: {str(ex)}") + return CallToolResult( + content=[TextContent(type="text", text=f"Tool execution failed: {str(ex)}")], + isError=True + ) + + async def _execute_tool(self, tool_instance: Any, arguments: Dict[str, Any]) -> Any: + """Execute a tool with the given arguments.""" + if hasattr(tool_instance, 'execute'): + if tool_instance.name == "describe_table": + return await tool_instance.execute(arguments.get("table_name", "")) + elif tool_instance.name in ["create_table", "drop_table", "read_data", "insert_data", "update_data"]: + return await tool_instance.execute(arguments.get("sql", "")) + else: + return await tool_instance.execute() + else: + raise ValueError(f"Tool {tool_instance.name} does not have an execute method") + + async def run(self): + """Run the MCP server.""" + try: + async with stdio_server() as (read_stream, write_stream): + await self.server.run( + read_stream, + write_stream, + self.server.create_initialization_options() + ) + except Exception as ex: + self.logger.error(f"Server error: {str(ex)}") + raise + + +async def main(): + """Main entry point for the MCP server.""" + server = MssqlMcpServer() + await server.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/MssqlMcp/python/src/mssql_mcp/sql_connection_factory.py b/MssqlMcp/python/src/mssql_mcp/sql_connection_factory.py new file mode 100644 index 0000000..3d10b98 --- /dev/null +++ b/MssqlMcp/python/src/mssql_mcp/sql_connection_factory.py @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +SQL connection factory for creating and managing database connections. +""" + +import os +import pyodbc +from abc import ABC, abstractmethod +from typing import Optional + + +class ISqlConnectionFactory(ABC): + """ + Abstract interface for creating SQL database connections. + """ + + @abstractmethod + async def get_open_connection(self) -> pyodbc.Connection: + """ + Get an open SQL database connection. + + Returns: + An open pyodbc.Connection object + + Raises: + Exception: If connection cannot be established + """ + pass + + +class SqlConnectionFactory(ISqlConnectionFactory): + """ + Factory for creating SQL Server database connections using pyodbc. + """ + + def __init__(self): + """Initialize the connection factory.""" + self._connection_string = self._get_connection_string() + + async def get_open_connection(self) -> pyodbc.Connection: + """ + Get an open SQL database connection. + + Returns: + An open pyodbc.Connection object + + Raises: + Exception: If connection cannot be established + """ + try: + # pyodbc connections are synchronous, but we wrap in async for consistency + connection = pyodbc.connect(self._connection_string) + return connection + except Exception as e: + raise Exception(f"Failed to connect to database: {str(e)}") + + def _get_connection_string(self) -> str: + """ + Get the connection string from environment variables. + + Returns: + Connection string for SQL Server + + Raises: + InvalidOperationException: If connection string is not set + """ + connection_string = os.getenv("CONNECTION_STRING") + + if not connection_string: + raise InvalidOperationException( + "Connection string is not set in the environment variable 'CONNECTION_STRING'.\n\n" + "HINT: Have a local SQL Server, with a database called 'test', from console, run " + "`export CONNECTION_STRING='Server=.;Database=test;Trusted_Connection=yes;TrustServerCertificate=yes'` " + "and then run the Python server" + ) + + return connection_string + + +class InvalidOperationException(Exception): + """Exception raised for invalid operations.""" + pass diff --git a/MssqlMcp/python/src/mssql_mcp/tools/__init__.py b/MssqlMcp/python/src/mssql_mcp/tools/__init__.py new file mode 100644 index 0000000..230fdc7 --- /dev/null +++ b/MssqlMcp/python/src/mssql_mcp/tools/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +Database tools for MSSQL MCP Server. +""" + +from .list_tables import ListTablesTool +from .describe_table import DescribeTableTool +from .create_table import CreateTableTool +from .drop_table import DropTableTool +from .read_data import ReadDataTool +from .insert_data import InsertDataTool +from .update_data import UpdateDataTool + +__all__ = [ + "ListTablesTool", + "DescribeTableTool", + "CreateTableTool", + "DropTableTool", + "ReadDataTool", + "InsertDataTool", + "UpdateDataTool" +] diff --git a/MssqlMcp/python/src/mssql_mcp/tools/create_table.py b/MssqlMcp/python/src/mssql_mcp/tools/create_table.py new file mode 100644 index 0000000..04cb5c2 --- /dev/null +++ b/MssqlMcp/python/src/mssql_mcp/tools/create_table.py @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +Create Table tool for MSSQL MCP Server. +""" + +import logging +from ..db_operation_result import DbOperationResult +from ..sql_connection_factory import ISqlConnectionFactory + + +class CreateTableTool: + """ + Tool for creating tables in the SQL Database. + """ + + def __init__(self, connection_factory: ISqlConnectionFactory, logger: logging.Logger): + """ + Initialize the CreateTableTool. + + Args: + connection_factory: Factory for creating database connections + logger: Logger instance for error logging + """ + self._connection_factory = connection_factory + self._logger = logger + + @property + def name(self) -> str: + """Get the tool name.""" + return "create_table" + + @property + def title(self) -> str: + """Get the tool title.""" + return "Create Table" + + @property + def description(self) -> str: + """Get the tool description.""" + return "Creates a new table in the SQL Database. Expects a valid CREATE TABLE SQL statement as input." + + @property + def readonly(self) -> bool: + """Indicate if this tool is read-only.""" + return False + + @property + def idempotent(self) -> bool: + """Indicate if this tool is idempotent.""" + return False + + @property + def destructive(self) -> bool: + """Indicate if this tool is destructive.""" + return False + + async def execute(self, sql: str) -> DbOperationResult: + """ + Execute the create table operation. + + Args: + sql: CREATE TABLE SQL statement + + Returns: + DbOperationResult indicating success or failure + """ + try: + connection = await self._connection_factory.get_open_connection() + + try: + cursor = connection.cursor() + cursor.execute(sql) + connection.commit() + + return DbOperationResult(success=True) + + finally: + connection.close() + + except Exception as ex: + self._logger.error(f"CreateTable failed: {str(ex)}") + return DbOperationResult(success=False, error=str(ex)) diff --git a/MssqlMcp/python/src/mssql_mcp/tools/describe_table.py b/MssqlMcp/python/src/mssql_mcp/tools/describe_table.py new file mode 100644 index 0000000..1dc611b --- /dev/null +++ b/MssqlMcp/python/src/mssql_mcp/tools/describe_table.py @@ -0,0 +1,242 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +Describe Table tool for MSSQL MCP Server. +""" + +import logging +from typing import Dict, Any, List, Optional +from ..db_operation_result import DbOperationResult +from ..sql_connection_factory import ISqlConnectionFactory + + +class DescribeTableTool: + """ + Tool for describing table schema in the SQL Database. + """ + + def __init__(self, connection_factory: ISqlConnectionFactory, logger: logging.Logger): + """ + Initialize the DescribeTableTool. + + Args: + connection_factory: Factory for creating database connections + logger: Logger instance for error logging + """ + self._connection_factory = connection_factory + self._logger = logger + + @property + def name(self) -> str: + """Get the tool name.""" + return "describe_table" + + @property + def title(self) -> str: + """Get the tool title.""" + return "Describe Table" + + @property + def description(self) -> str: + """Get the tool description.""" + return "Returns table schema" + + @property + def readonly(self) -> bool: + """Indicate if this tool is read-only.""" + return True + + @property + def idempotent(self) -> bool: + """Indicate if this tool is idempotent.""" + return True + + @property + def destructive(self) -> bool: + """Indicate if this tool is destructive.""" + return False + + async def execute(self, table_name: str) -> DbOperationResult: + """ + Execute the describe table operation. + + Args: + table_name: Name of the table to describe (can include schema.table format) + + Returns: + DbOperationResult containing the table schema information + """ + # Parse schema and table name + schema = None + if '.' in table_name: + parts = table_name.split('.') + if len(parts) > 1: + schema = parts[0] + table_name = parts[1] + + # Query for table metadata + TABLE_INFO_QUERY = """ + SELECT t.object_id AS id, t.name, s.name AS [schema], p.value AS description, t.type, u.name AS owner + FROM sys.tables t + INNER JOIN sys.schemas s ON t.schema_id = s.schema_id + LEFT JOIN sys.extended_properties p ON p.major_id = t.object_id AND p.minor_id = 0 AND p.name = 'MS_Description' + LEFT JOIN sys.sysusers u ON t.principal_id = u.uid + WHERE t.name = ? AND (s.name = ? OR ? IS NULL) + """ + + # Query for columns + COLUMNS_QUERY = """ + SELECT c.name, ty.name AS type, c.max_length AS length, c.precision, c.is_nullable AS nullable, p.value AS description + FROM sys.columns c + INNER JOIN sys.types ty ON c.user_type_id = ty.user_type_id + LEFT JOIN sys.extended_properties p ON p.major_id = c.object_id AND p.minor_id = c.column_id AND p.name = 'MS_Description' + WHERE c.object_id = ( + SELECT object_id FROM sys.tables t + INNER JOIN sys.schemas s ON t.schema_id = s.schema_id + WHERE t.name = ? AND (s.name = ? OR ? IS NULL) + ) + """ + + # Query for indexes + INDEXES_QUERY = """ + SELECT i.name, i.type_desc AS type, p.value AS description, + STUFF(( + SELECT ',' + c.name + FROM sys.index_columns ic + INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id + WHERE ic.object_id = i.object_id AND ic.index_id = i.index_id + ORDER BY ic.key_ordinal + FOR XML PATH('') + ), 1, 1, '') AS keys + FROM sys.indexes i + LEFT JOIN sys.extended_properties p ON p.major_id = i.object_id AND p.minor_id = i.index_id AND p.name = 'MS_Description' + WHERE i.object_id = ( + SELECT object_id FROM sys.tables t + INNER JOIN sys.schemas s ON t.schema_id = s.schema_id + WHERE t.name = ? AND (s.name = ? OR ? IS NULL) + ) AND i.is_primary_key = 0 AND i.is_unique_constraint = 0 + """ + + # Query for constraints + CONSTRAINTS_QUERY = """ + SELECT kc.name, kc.type_desc AS type, + STUFF(( + SELECT ',' + c.name + FROM sys.index_columns ic + INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id + WHERE ic.object_id = kc.parent_object_id AND ic.index_id = kc.unique_index_id + ORDER BY ic.key_ordinal + FOR XML PATH('') + ), 1, 1, '') AS keys + FROM sys.key_constraints kc + WHERE kc.parent_object_id = ( + SELECT object_id FROM sys.tables t + INNER JOIN sys.schemas s ON t.schema_id = s.schema_id + WHERE t.name = ? AND (s.name = ? OR ? IS NULL) + ) + """ + + # Query for foreign keys + FOREIGN_KEY_QUERY = """ + SELECT + fk.name AS name, + SCHEMA_NAME(tp.schema_id) AS [schema], + tp.name AS table_name, + STRING_AGG(cp.name, ', ') WITHIN GROUP (ORDER BY fkc.constraint_column_id) AS column_names, + SCHEMA_NAME(tr.schema_id) AS referenced_schema, + tr.name AS referenced_table, + STRING_AGG(cr.name, ', ') WITHIN GROUP (ORDER BY fkc.constraint_column_id) AS referenced_column_names + FROM sys.foreign_keys AS fk + JOIN sys.foreign_key_columns AS fkc ON fk.object_id = fkc.constraint_object_id + JOIN sys.tables AS tp ON fkc.parent_object_id = tp.object_id + JOIN sys.columns AS cp ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id + JOIN sys.tables AS tr ON fkc.referenced_object_id = tr.object_id + JOIN sys.columns AS cr ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id + WHERE (SCHEMA_NAME(tp.schema_id) = ? OR ? IS NULL) AND tp.name = ? + GROUP BY fk.name, tp.schema_id, tp.name, tr.schema_id, tr.name + """ + + try: + connection = await self._connection_factory.get_open_connection() + + try: + cursor = connection.cursor() + result: Dict[str, Any] = {} + + # Get table info + cursor.execute(TABLE_INFO_QUERY, (table_name, schema, schema)) + table_row = cursor.fetchone() + + if not table_row: + return DbOperationResult(success=False, error=f"Table '{table_name}' not found.") + + result["table"] = { + "id": table_row[0], + "name": table_row[1], + "schema": table_row[2], + "owner": table_row[5], + "type": table_row[4], + "description": table_row[3] if table_row[3] else None + } + + # Get columns + cursor.execute(COLUMNS_QUERY, (table_name, schema, schema)) + columns = [] + for row in cursor.fetchall(): + columns.append({ + "name": row[0], + "type": row[1], + "length": row[2], + "precision": row[3], + "nullable": bool(row[4]), + "description": row[5] if row[5] else None + }) + result["columns"] = columns + + # Get indexes + cursor.execute(INDEXES_QUERY, (table_name, schema, schema)) + indexes = [] + for row in cursor.fetchall(): + indexes.append({ + "name": row[0], + "type": row[1], + "description": row[2] if row[2] else None, + "keys": row[3] + }) + result["indexes"] = indexes + + # Get constraints + cursor.execute(CONSTRAINTS_QUERY, (table_name, schema, schema)) + constraints = [] + for row in cursor.fetchall(): + constraints.append({ + "name": row[0], + "type": row[1], + "keys": row[2] + }) + result["constraints"] = constraints + + # Get foreign keys + cursor.execute(FOREIGN_KEY_QUERY, (schema, schema, table_name)) + foreign_keys = [] + for row in cursor.fetchall(): + foreign_keys.append({ + "name": row[0], + "schema": row[1], + "table_name": row[2], + "column_name": row[3], + "referenced_schema": row[4], + "referenced_table": row[5], + "referenced_column": row[6] + }) + result["foreignKeys"] = foreign_keys + + return DbOperationResult(success=True, data=result) + + finally: + connection.close() + + except Exception as ex: + self._logger.error(f"DescribeTable failed: {str(ex)}") + return DbOperationResult(success=False, error=str(ex)) diff --git a/MssqlMcp/python/src/mssql_mcp/tools/drop_table.py b/MssqlMcp/python/src/mssql_mcp/tools/drop_table.py new file mode 100644 index 0000000..b6a78f2 --- /dev/null +++ b/MssqlMcp/python/src/mssql_mcp/tools/drop_table.py @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +Drop Table tool for MSSQL MCP Server. +""" + +import logging +from ..db_operation_result import DbOperationResult +from ..sql_connection_factory import ISqlConnectionFactory + + +class DropTableTool: + """ + Tool for dropping tables in the SQL Database. + """ + + def __init__(self, connection_factory: ISqlConnectionFactory, logger: logging.Logger): + """ + Initialize the DropTableTool. + + Args: + connection_factory: Factory for creating database connections + logger: Logger instance for error logging + """ + self._connection_factory = connection_factory + self._logger = logger + + @property + def name(self) -> str: + """Get the tool name.""" + return "drop_table" + + @property + def title(self) -> str: + """Get the tool title.""" + return "Drop Table" + + @property + def description(self) -> str: + """Get the tool description.""" + return "Drops a table in the SQL Database. Expects a valid DROP TABLE SQL statement as input." + + @property + def readonly(self) -> bool: + """Indicate if this tool is read-only.""" + return False + + @property + def idempotent(self) -> bool: + """Indicate if this tool is idempotent.""" + return False + + @property + def destructive(self) -> bool: + """Indicate if this tool is destructive.""" + return True + + async def execute(self, sql: str) -> DbOperationResult: + """ + Execute the drop table operation. + + Args: + sql: DROP TABLE SQL statement + + Returns: + DbOperationResult indicating success or failure + """ + try: + connection = await self._connection_factory.get_open_connection() + + try: + cursor = connection.cursor() + cursor.execute(sql) + connection.commit() + + return DbOperationResult(success=True) + + finally: + connection.close() + + except Exception as ex: + self._logger.error(f"DropTable failed: {str(ex)}") + return DbOperationResult(success=False, error=str(ex)) diff --git a/MssqlMcp/python/src/mssql_mcp/tools/insert_data.py b/MssqlMcp/python/src/mssql_mcp/tools/insert_data.py new file mode 100644 index 0000000..e0d20c3 --- /dev/null +++ b/MssqlMcp/python/src/mssql_mcp/tools/insert_data.py @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +Insert Data tool for MSSQL MCP Server. +""" + +import logging +from ..db_operation_result import DbOperationResult +from ..sql_connection_factory import ISqlConnectionFactory + + +class InsertDataTool: + """ + Tool for inserting data into the SQL Database. + """ + + def __init__(self, connection_factory: ISqlConnectionFactory, logger: logging.Logger): + """ + Initialize the InsertDataTool. + + Args: + connection_factory: Factory for creating database connections + logger: Logger instance for error logging + """ + self._connection_factory = connection_factory + self._logger = logger + + @property + def name(self) -> str: + """Get the tool name.""" + return "insert_data" + + @property + def title(self) -> str: + """Get the tool title.""" + return "Insert Data" + + @property + def description(self) -> str: + """Get the tool description.""" + return "Inserts data into a table in the SQL Database. Expects a valid INSERT SQL statement as input." + + @property + def readonly(self) -> bool: + """Indicate if this tool is read-only.""" + return False + + @property + def idempotent(self) -> bool: + """Indicate if this tool is idempotent.""" + return False + + @property + def destructive(self) -> bool: + """Indicate if this tool is destructive.""" + return False + + async def execute(self, sql: str) -> DbOperationResult: + """ + Execute the insert data operation. + + Args: + sql: INSERT SQL statement + + Returns: + DbOperationResult indicating success or failure with rows affected + """ + try: + connection = await self._connection_factory.get_open_connection() + + try: + cursor = connection.cursor() + rows_affected = cursor.execute(sql).rowcount + connection.commit() + + return DbOperationResult(success=True, rows_affected=rows_affected) + + finally: + connection.close() + + except Exception as ex: + self._logger.error(f"InsertData failed: {str(ex)}") + return DbOperationResult(success=False, error=str(ex)) diff --git a/MssqlMcp/python/src/mssql_mcp/tools/list_tables.py b/MssqlMcp/python/src/mssql_mcp/tools/list_tables.py new file mode 100644 index 0000000..5332387 --- /dev/null +++ b/MssqlMcp/python/src/mssql_mcp/tools/list_tables.py @@ -0,0 +1,93 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +List Tables tool for MSSQL MCP Server. +""" + +import logging +from typing import List +from ..db_operation_result import DbOperationResult +from ..sql_connection_factory import ISqlConnectionFactory + + +class ListTablesTool: + """ + Tool for listing all tables in the SQL Database. + """ + + def __init__(self, connection_factory: ISqlConnectionFactory, logger: logging.Logger): + """ + Initialize the ListTablesTool. + + Args: + connection_factory: Factory for creating database connections + logger: Logger instance for error logging + """ + self._connection_factory = connection_factory + self._logger = logger + + @property + def name(self) -> str: + """Get the tool name.""" + return "list_tables" + + @property + def title(self) -> str: + """Get the tool title.""" + return "List Tables" + + @property + def description(self) -> str: + """Get the tool description.""" + return "Lists all tables in the SQL Database." + + @property + def readonly(self) -> bool: + """Indicate if this tool is read-only.""" + return True + + @property + def idempotent(self) -> bool: + """Indicate if this tool is idempotent.""" + return True + + @property + def destructive(self) -> bool: + """Indicate if this tool is destructive.""" + return False + + async def execute(self) -> DbOperationResult: + """ + Execute the list tables operation. + + Returns: + DbOperationResult containing the list of tables + """ + LIST_TABLES_QUERY = """ + SELECT TABLE_SCHEMA, TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_TYPE = 'BASE TABLE' + ORDER BY TABLE_SCHEMA, TABLE_NAME + """ + + try: + connection = await self._connection_factory.get_open_connection() + + try: + cursor = connection.cursor() + cursor.execute(LIST_TABLES_QUERY) + + tables = [] + for row in cursor.fetchall(): + schema, table_name = row + tables.append(f"{schema}.{table_name}") + + return DbOperationResult(success=True, data=tables) + + finally: + connection.close() + + except Exception as ex: + self._logger.error(f"ListTables failed: {str(ex)}") + return DbOperationResult(success=False, error=str(ex)) diff --git a/MssqlMcp/python/src/mssql_mcp/tools/read_data.py b/MssqlMcp/python/src/mssql_mcp/tools/read_data.py new file mode 100644 index 0000000..cd0d777 --- /dev/null +++ b/MssqlMcp/python/src/mssql_mcp/tools/read_data.py @@ -0,0 +1,99 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +Read Data tool for MSSQL MCP Server. +""" + +import logging +from typing import List, Dict, Any, Optional +from ..db_operation_result import DbOperationResult +from ..sql_connection_factory import ISqlConnectionFactory + + +class ReadDataTool: + """ + Tool for reading data from the SQL Database. + """ + + def __init__(self, connection_factory: ISqlConnectionFactory, logger: logging.Logger): + """ + Initialize the ReadDataTool. + + Args: + connection_factory: Factory for creating database connections + logger: Logger instance for error logging + """ + self._connection_factory = connection_factory + self._logger = logger + + @property + def name(self) -> str: + """Get the tool name.""" + return "read_data" + + @property + def title(self) -> str: + """Get the tool title.""" + return "Read Data" + + @property + def description(self) -> str: + """Get the tool description.""" + return "Executes SQL queries against SQL Database to read data" + + @property + def readonly(self) -> bool: + """Indicate if this tool is read-only.""" + return True + + @property + def idempotent(self) -> bool: + """Indicate if this tool is idempotent.""" + return True + + @property + def destructive(self) -> bool: + """Indicate if this tool is destructive.""" + return False + + async def execute(self, sql: str) -> DbOperationResult: + """ + Execute the read data operation. + + Args: + sql: SQL query to execute + + Returns: + DbOperationResult containing the query results + """ + try: + connection = await self._connection_factory.get_open_connection() + + try: + cursor = connection.cursor() + cursor.execute(sql) + + # Get column names + columns = [column[0] for column in cursor.description] if cursor.description else [] + + # Fetch all rows + rows = cursor.fetchall() + + # Convert rows to list of dictionaries + results = [] + for row in rows: + row_dict = {} + for i, value in enumerate(row): + column_name = columns[i] if i < len(columns) else f"column_{i}" + row_dict[column_name] = value + results.append(row_dict) + + return DbOperationResult(success=True, data=results) + + finally: + connection.close() + + except Exception as ex: + self._logger.error(f"ReadData failed: {str(ex)}") + return DbOperationResult(success=False, error=str(ex)) diff --git a/MssqlMcp/python/src/mssql_mcp/tools/update_data.py b/MssqlMcp/python/src/mssql_mcp/tools/update_data.py new file mode 100644 index 0000000..7eef298 --- /dev/null +++ b/MssqlMcp/python/src/mssql_mcp/tools/update_data.py @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. + +""" +Update Data tool for MSSQL MCP Server. +""" + +import logging +from ..db_operation_result import DbOperationResult +from ..sql_connection_factory import ISqlConnectionFactory + + +class UpdateDataTool: + """ + Tool for updating data in the SQL Database. + """ + + def __init__(self, connection_factory: ISqlConnectionFactory, logger: logging.Logger): + """ + Initialize the UpdateDataTool. + + Args: + connection_factory: Factory for creating database connections + logger: Logger instance for error logging + """ + self._connection_factory = connection_factory + self._logger = logger + + @property + def name(self) -> str: + """Get the tool name.""" + return "update_data" + + @property + def title(self) -> str: + """Get the tool title.""" + return "Update Data" + + @property + def description(self) -> str: + """Get the tool description.""" + return "Updates data in a table in the SQL Database. Expects a valid UPDATE SQL statement as input." + + @property + def readonly(self) -> bool: + """Indicate if this tool is read-only.""" + return False + + @property + def idempotent(self) -> bool: + """Indicate if this tool is idempotent.""" + return False + + @property + def destructive(self) -> bool: + """Indicate if this tool is destructive.""" + return True + + async def execute(self, sql: str) -> DbOperationResult: + """ + Execute the update data operation. + + Args: + sql: UPDATE SQL statement + + Returns: + DbOperationResult indicating success or failure with rows affected + """ + try: + connection = await self._connection_factory.get_open_connection() + + try: + cursor = connection.cursor() + rows_affected = cursor.execute(sql).rowcount + connection.commit() + + return DbOperationResult(success=True, rows_affected=rows_affected) + + finally: + connection.close() + + except Exception as ex: + self._logger.error(f"UpdateData failed: {str(ex)}") + return DbOperationResult(success=False, error=str(ex))