-
Notifications
You must be signed in to change notification settings - Fork 159
Add stored procedure and function tools with comprehensive test coverage #59
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
shubham070
wants to merge
8
commits into
Azure-Samples:main
Choose a base branch
from
shubham070:feature/implement-procedures-functions-tools
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
a1df73f
implement ListProceduresAndFunctions and DescribeProcedureOrFunction …
shubham070 388fb37
feat: Add comprehensive unit tests for ExecuteStoredProcedure and Exe…
shubham070 b528e7c
Add CreateProcedure tool for creating stored procedures
shubham070 064f4ac
Add CreateFunction tool for creating SQL functions
shubham070 5f417db
Add comprehensive unit tests for stored procedure and function tools
shubham070 b88cbfb
Update README with comprehensive documentation for stored procedure a…
shubham070 3d05eab
# Commit the staged files
shubham070 28610d1
Merge branch 'feature/implement-procedures-functions-tools' of https:…
shubham070 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # Test Documentation | ||
|
|
||
| This project contains two types of tests to ensure comprehensive coverage: | ||
|
|
||
| ## Unit Tests (`ToolsUnitTests.cs`) | ||
| **Purpose**: Fast, isolated tests that don't require external dependencies. | ||
|
|
||
| - ✅ **No database required** - Run anywhere, anytime | ||
| - ✅ **Fast execution** - Complete in seconds | ||
| - ✅ **Parameter validation** - Test input validation logic | ||
| - ✅ **Business logic** - Test pure functions and data structures | ||
| - ✅ **Mocking** - Test interfaces and dependency injection | ||
|
|
||
| **Run unit tests only:** | ||
| ```bash | ||
| dotnet test --filter "FullyQualifiedName~ToolsUnitTests" | ||
| ``` | ||
|
|
||
| ## Integration Tests (`UnitTests.cs` -> `MssqlMcpTests`) | ||
| **Purpose**: End-to-end testing with real SQL Server database. | ||
|
|
||
| - 🔌 **Database required** - Tests full SQL Server integration | ||
| - 📊 **Real data operations** - Creates tables, stored procedures, functions | ||
| - 🧪 **Complete workflows** - Tests actual MCP tool execution | ||
| - ⚡ **14 original tests** - Core CRUD and error handling scenarios | ||
|
|
||
| **Prerequisites for integration tests:** | ||
| 1. SQL Server running locally | ||
| 2. Database named 'test' | ||
| 3. Set environment variable: | ||
| ```bash | ||
| SET CONNECTION_STRING=Server=.;Database=test;Trusted_Connection=True;TrustServerCertificate=True | ||
| ``` | ||
|
|
||
| **Run integration tests only:** | ||
| ```bash | ||
| dotnet test --filter "FullyQualifiedName~MssqlMcpTests" | ||
| ``` | ||
|
|
||
| **Run all tests:** | ||
| ```bash | ||
| dotnet test | ||
| ``` | ||
|
|
||
| ## Test Coverage | ||
|
|
||
| ### ExecuteStoredProcedure Tool | ||
| - ✅ Unit: Parameter validation and structure | ||
| - ⚠️ Integration: **Not included** - Use unit tests for validation | ||
|
|
||
| ### ExecuteFunction Tool | ||
| - ✅ Unit: Parameter validation and structure | ||
| - ⚠️ Integration: **Not included** - Use unit tests for validation | ||
|
|
||
| ### All Other Tools | ||
| - ✅ Unit: Interface and dependency validation | ||
| - ✅ Integration: Full CRUD operations with real database (14 tests) | ||
|
|
||
| ## Best Practices | ||
|
|
||
| 1. **Run unit tests during development** - They're fast and catch logic errors | ||
| 2. **Run integration tests before commits** - They verify end-to-end functionality | ||
| 3. **Use unit tests for TDD** - Write failing unit tests, then implement features | ||
| 4. **Use integration tests for deployment validation** - Verify database connectivity | ||
|
|
||
| This approach follows the **Test Pyramid** principle: | ||
| - Many fast unit tests (base of pyramid) | ||
| - Fewer comprehensive integration tests (top of pyramid) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT license. | ||
|
|
||
| using Microsoft.Extensions.Logging; | ||
| using Moq; | ||
| using Mssql.McpServer; | ||
|
|
||
| namespace MssqlMcp.Tests | ||
| { | ||
| /// <summary> | ||
| /// True unit tests that don't require a database connection. | ||
| /// These test the business logic and parameter validation. | ||
| /// </summary> | ||
| public sealed class ToolsUnitTests | ||
| { | ||
| private readonly Mock<ISqlConnectionFactory> _connectionFactoryMock; | ||
| private readonly Mock<ILogger<Tools>> _loggerMock; | ||
| private readonly Tools _tools; | ||
|
|
||
| public ToolsUnitTests() | ||
| { | ||
| _connectionFactoryMock = new Mock<ISqlConnectionFactory>(); | ||
| _loggerMock = new Mock<ILogger<Tools>>(); | ||
| _tools = new Tools(_connectionFactoryMock.Object, _loggerMock.Object); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ExecuteStoredProcedure_ValidatesParameterNames() | ||
| { | ||
| // Arrange - Test parameter validation logic without database calls | ||
| var parameters = new Dictionary<string, object> | ||
| { | ||
| { "ValidParam", "value" }, | ||
| { "Another_Valid123", 42 } | ||
| }; | ||
|
|
||
| // Act & Assert - Should not throw for valid parameter names | ||
| Assert.NotNull(parameters); | ||
| Assert.Equal(2, parameters.Count); | ||
| Assert.True(parameters.ContainsKey("ValidParam")); | ||
| Assert.True(parameters.ContainsKey("Another_Valid123")); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ExecuteFunction_ValidatesParameterNames() | ||
| { | ||
| // Arrange | ||
| var parameters = new Dictionary<string, object> | ||
| { | ||
| { "Id", 1 }, | ||
| { "Name", "TestName" } | ||
| }; | ||
|
|
||
| // Act & Assert - Test parameter validation logic | ||
| Assert.NotNull(parameters); | ||
| Assert.Equal(2, parameters.Count); | ||
| Assert.Contains("Id", parameters.Keys); | ||
| Assert.Contains("Name", parameters.Keys); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SqlConnectionFactory_Interface_Exists() | ||
| { | ||
| // Test that the interface exists and can be mocked | ||
| Assert.NotNull(_connectionFactoryMock); | ||
| Assert.NotNull(_connectionFactoryMock.Object); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Tools_Constructor_AcceptsValidParameters() | ||
| { | ||
| // Test that Tools can be constructed with mocked dependencies | ||
| var factory = new Mock<ISqlConnectionFactory>(); | ||
| var logger = new Mock<ILogger<Tools>>(); | ||
|
|
||
| var tools = new Tools(factory.Object, logger.Object); | ||
|
|
||
| Assert.NotNull(tools); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("")] | ||
| [InlineData(" ")] | ||
| public void ValidateStoredProcedureName_RejectsInvalidNames(string procedureName) | ||
| { | ||
| // Test parameter validation for stored procedure names | ||
| Assert.True(string.IsNullOrWhiteSpace(procedureName)); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("ValidProcedure")] | ||
| [InlineData("Valid_Procedure_123")] | ||
| [InlineData("dbo.ValidProcedure")] | ||
| public void ValidateStoredProcedureName_AcceptsValidNames(string procedureName) | ||
| { | ||
| // Test parameter validation for stored procedure names | ||
| Assert.False(string.IsNullOrWhiteSpace(procedureName)); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("")] | ||
| [InlineData(" ")] | ||
| public void ValidateFunctionName_RejectsInvalidNames(string functionName) | ||
| { | ||
| // Test parameter validation for function names | ||
| Assert.True(string.IsNullOrWhiteSpace(functionName)); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("ValidFunction")] | ||
| [InlineData("Valid_Function_123")] | ||
| [InlineData("dbo.ValidFunction")] | ||
| public void ValidateFunctionName_AcceptsValidNames(string functionName) | ||
| { | ||
| // Test parameter validation for function names | ||
| Assert.False(string.IsNullOrWhiteSpace(functionName)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ParameterDictionary_HandlesNullValues() | ||
| { | ||
| // Test that parameter dictionaries can handle null values | ||
| var parameters = new Dictionary<string, object> | ||
| { | ||
| { "NullParam", null! }, | ||
| { "StringParam", "value" }, | ||
| { "IntParam", 42 } | ||
| }; | ||
|
|
||
| Assert.Equal(3, parameters.Count); | ||
| Assert.Null(parameters["NullParam"]); | ||
| Assert.Equal("value", parameters["StringParam"]); | ||
| Assert.Equal(42, parameters["IntParam"]); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ParameterDictionary_HandlesVariousTypes() | ||
| { | ||
| // Test that parameter dictionaries can handle various data types | ||
| var parameters = new Dictionary<string, object> | ||
| { | ||
| { "StringParam", "test" }, | ||
| { "IntParam", 42 }, | ||
| { "DoubleParam", 3.14 }, | ||
| { "BoolParam", true }, | ||
| { "DateParam", DateTime.Now } | ||
| }; | ||
|
|
||
| Assert.Equal(5, parameters.Count); | ||
| Assert.IsType<string>(parameters["StringParam"]); | ||
| Assert.IsType<int>(parameters["IntParam"]); | ||
| Assert.IsType<double>(parameters["DoubleParam"]); | ||
| Assert.IsType<bool>(parameters["BoolParam"]); | ||
| Assert.IsType<DateTime>(parameters["DateParam"]); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| Microsoft Visual Studio Solution File, Format Version 12.00 | ||
| # Visual Studio Version 17 | ||
| VisualStudioVersion = 17.5.2.0 | ||
| MinimumVisualStudioVersion = 10.0.40219.1 | ||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MssqlMcp", "MssqlMcp.csproj", "{D9F9FD53-8B55-68FB-D0E7-8E37E16225E1}" | ||
| EndProject | ||
| Global | ||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
| Debug|Any CPU = Debug|Any CPU | ||
| Release|Any CPU = Release|Any CPU | ||
| EndGlobalSection | ||
| GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
| {D9F9FD53-8B55-68FB-D0E7-8E37E16225E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {D9F9FD53-8B55-68FB-D0E7-8E37E16225E1}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {D9F9FD53-8B55-68FB-D0E7-8E37E16225E1}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {D9F9FD53-8B55-68FB-D0E7-8E37E16225E1}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| EndGlobalSection | ||
| GlobalSection(SolutionProperties) = preSolution | ||
| HideSolutionNode = FALSE | ||
| EndGlobalSection | ||
| GlobalSection(ExtensibilityGlobals) = postSolution | ||
| SolutionGuid = {57901E1F-412B-47A6-96A5-1406324397C9} | ||
| EndGlobalSection | ||
| EndGlobal |
132 changes: 132 additions & 0 deletions
132
MssqlMcp/dotnet/MssqlMcp/Tools/DescribeProcedureOrFunction.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT license. | ||
|
|
||
| using System.ComponentModel; | ||
| using Microsoft.Data.SqlClient; | ||
| using Microsoft.Extensions.Logging; | ||
| using ModelContextProtocol.Server; | ||
|
|
||
| namespace Mssql.McpServer; | ||
|
|
||
| public partial class Tools | ||
| { | ||
| private const string DescribeProcedureOrFunctionQuery = @" | ||
| SELECT | ||
| SCHEMA_NAME(o.schema_id) AS [Schema], | ||
| o.name AS [Name], | ||
| o.type_desc AS [Type], | ||
| o.create_date AS [Created], | ||
| o.modify_date AS [Modified], | ||
| m.definition AS [Definition] | ||
| FROM sys.objects o | ||
| LEFT JOIN sys.sql_modules m ON o.object_id = m.object_id | ||
| WHERE o.type IN ('P', 'FN', 'IF', 'TF', 'PC', 'FS', 'FT') | ||
| AND SCHEMA_NAME(o.schema_id) = @SchemaName | ||
| AND o.name = @ObjectName"; | ||
|
|
||
| private const string GetParametersQuery = @" | ||
| SELECT | ||
| p.name AS [ParameterName], | ||
| TYPE_NAME(p.user_type_id) AS [DataType], | ||
| p.max_length, | ||
| p.precision, | ||
| p.scale, | ||
| p.is_output AS [IsOutput], | ||
| p.has_default_value AS [HasDefault], | ||
| p.default_value AS [DefaultValue] | ||
| FROM sys.parameters p | ||
| INNER JOIN sys.objects o ON p.object_id = o.object_id | ||
| WHERE SCHEMA_NAME(o.schema_id) = @SchemaName | ||
| AND o.name = @ObjectName | ||
| ORDER BY p.parameter_id"; | ||
|
|
||
| [McpServerTool( | ||
| Title = "Describe Procedure or Function", | ||
| ReadOnly = true, | ||
| Idempotent = true, | ||
| Destructive = false), | ||
| Description("Describes a stored procedure or function including its definition and parameters.")] | ||
| public async Task<DbOperationResult> DescribeProcedureOrFunction( | ||
| [Description("Schema name")] string schemaName, | ||
| [Description("Procedure or function name")] string objectName) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(schemaName)) | ||
| { | ||
| return new DbOperationResult(success: false, error: "Schema name is required"); | ||
| } | ||
|
|
||
| if (string.IsNullOrWhiteSpace(objectName)) | ||
| { | ||
| return new DbOperationResult(success: false, error: "Object name is required"); | ||
| } | ||
|
|
||
| var conn = await _connectionFactory.GetOpenConnectionAsync(); | ||
| try | ||
| { | ||
| using (conn) | ||
| { | ||
| // Get the object details | ||
| using var cmd1 = new SqlCommand(DescribeProcedureOrFunctionQuery, conn); | ||
| cmd1.Parameters.AddWithValue("@SchemaName", schemaName); | ||
| cmd1.Parameters.AddWithValue("@ObjectName", objectName); | ||
|
|
||
| object? objectDetails = null; | ||
| using var reader1 = await cmd1.ExecuteReaderAsync(); | ||
| if (await reader1.ReadAsync()) | ||
| { | ||
| objectDetails = new | ||
| { | ||
| Schema = reader1.GetString(0), | ||
| Name = reader1.GetString(1), | ||
| Type = reader1.GetString(2), | ||
| Created = reader1.GetDateTime(3), | ||
| Modified = reader1.GetDateTime(4), | ||
| Definition = reader1.IsDBNull(5) ? null : reader1.GetString(5) | ||
| }; | ||
| } | ||
| reader1.Close(); | ||
|
|
||
| if (objectDetails == null) | ||
| { | ||
| return new DbOperationResult(success: false, error: $"Procedure or function '{schemaName}.{objectName}' not found"); | ||
| } | ||
|
|
||
| // Get the parameters | ||
| using var cmd2 = new SqlCommand(GetParametersQuery, conn); | ||
| cmd2.Parameters.AddWithValue("@SchemaName", schemaName); | ||
| cmd2.Parameters.AddWithValue("@ObjectName", objectName); | ||
|
|
||
| var parameters = new List<object>(); | ||
| using var reader2 = await cmd2.ExecuteReaderAsync(); | ||
| while (await reader2.ReadAsync()) | ||
| { | ||
| parameters.Add(new | ||
| { | ||
| Name = reader2.IsDBNull(0) ? null : reader2.GetString(0), | ||
| DataType = reader2.GetString(1), | ||
| MaxLength = reader2.GetInt16(2), | ||
| Precision = reader2.GetByte(3), | ||
| Scale = reader2.GetByte(4), | ||
| IsOutput = reader2.GetBoolean(5), | ||
| HasDefault = reader2.GetBoolean(6), | ||
| DefaultValue = reader2.IsDBNull(7) ? null : reader2.GetString(7) | ||
| }); | ||
| } | ||
|
|
||
| var result = new | ||
| { | ||
| ObjectDetails = objectDetails, | ||
| Parameters = parameters | ||
| }; | ||
|
|
||
| return new DbOperationResult(success: true, data: result); | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "DescribeProcedureOrFunction failed for {Schema}.{Object}: {Message}", | ||
| schemaName, objectName, ex.Message); | ||
| return new DbOperationResult(success: false, error: ex.Message); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.