Skip to content
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
423 changes: 423 additions & 0 deletions MssqlMcp/dotnet/MssqlMcp.Tests/ReadDataToolTests.cs

Large diffs are not rendered by default.

146 changes: 119 additions & 27 deletions MssqlMcp/dotnet/MssqlMcp.Tests/UnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@

namespace MssqlMcp.Tests
{
[Collection("Database Tests")]
public sealed class MssqlMcpTests : IDisposable
{
private readonly string _tableName;
private readonly Tools _tools;
public MssqlMcpTests()
{
_tableName = $"TestTable_{Guid.NewGuid():N}";
_tableName = $"UnitTest_{Guid.NewGuid():N}";
var connectionFactory = new SqlConnectionFactory();
var loggerMock = new Mock<ILogger<Tools>>();
_tools = new Tools(connectionFactory, loggerMock.Object);
Expand Down Expand Up @@ -93,23 +94,6 @@ public async Task ListTables_ReturnsTables()
Assert.NotNull(result.Data);
}

[Fact]
public async Task ReadData_ReturnsData_WhenSqlIsValid()
{
// Ensure table exists and has data
var createResult = await _tools.CreateTable($"CREATE TABLE {_tableName} (Id INT PRIMARY KEY)") as DbOperationResult;
Assert.NotNull(createResult);
Assert.True(createResult.Success);
var insertResult = await _tools.InsertData($"INSERT INTO {_tableName} (Id) VALUES (1)") as DbOperationResult;
Assert.NotNull(insertResult);
Assert.True(insertResult.Success);

var sql = $"SELECT * FROM {_tableName}";
var result = await _tools.ReadData(sql) as DbOperationResult;
Assert.NotNull(result);
Assert.True(result.Success);
Assert.NotNull(result.Data);
}

[Fact]
public async Task UpdateData_ReturnsSuccess_WhenSqlIsValid()
Expand Down Expand Up @@ -168,15 +152,6 @@ public async Task InsertData_ReturnsError_WhenSqlIsInvalid()
Assert.Contains("syntax", result.Error ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public async Task ReadData_ReturnsError_WhenSqlIsInvalid()
{
var sql = "SELECT FROM";
var result = await _tools.ReadData(sql) as DbOperationResult;
Assert.NotNull(result);
Assert.False(result.Success);
Assert.Contains("syntax", result.Error ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public async Task UpdateData_ReturnsError_WhenSqlIsInvalid()
Expand Down Expand Up @@ -210,5 +185,122 @@ public async Task SqlInjection_NotExecuted_When_QueryFails()
Assert.NotNull(describeResult);
Assert.True(describeResult.Success);
}

[Fact]
public async Task ReadOnlyMode_ListTables_ReturnsSuccess_WhenReadOnlyIsTrue()
{
// Set READONLY environment variable
Environment.SetEnvironmentVariable("READONLY", "true");
try
{
var result = await _tools.ListTables() as DbOperationResult;
Assert.NotNull(result);
Assert.True(result.Success);
Assert.NotNull(result.Data);
}
finally
{
// Clean up environment variable
Environment.SetEnvironmentVariable("READONLY", null);
}
}

[Fact]
public async Task ReadOnlyMode_CreateTable_ReturnsError_WhenReadOnlyIsTrue()
{
// Set READONLY environment variable
Environment.SetEnvironmentVariable("READONLY", "true");
try
{
var sql = $"CREATE TABLE {_tableName} (Id INT PRIMARY KEY)";
var result = await _tools.CreateTable(sql) as DbOperationResult;
Assert.NotNull(result);
Assert.False(result.Success);
Assert.Contains("CREATE TABLE operation is not allowed in READONLY mode", result.Error ?? string.Empty);
}
finally
{
// Clean up environment variable
Environment.SetEnvironmentVariable("READONLY", null);
}
}

[Fact]
public async Task ReadOnlyMode_InsertData_ReturnsError_WhenReadOnlyIsTrue()
{
// Set READONLY environment variable
Environment.SetEnvironmentVariable("READONLY", "true");
try
{
var sql = $"INSERT INTO {_tableName} (Id) VALUES (1)";
var result = await _tools.InsertData(sql) as DbOperationResult;
Assert.NotNull(result);
Assert.False(result.Success);
Assert.Contains("INSERT operation is not allowed in READONLY mode", result.Error ?? string.Empty);
}
finally
{
// Clean up environment variable
Environment.SetEnvironmentVariable("READONLY", null);
}
}

[Fact]
public async Task ReadOnlyMode_UpdateData_ReturnsError_WhenReadOnlyIsTrue()
{
// Set READONLY environment variable
Environment.SetEnvironmentVariable("READONLY", "true");
try
{
var sql = $"UPDATE {_tableName} SET Id = 2 WHERE Id = 1";
var result = await _tools.UpdateData(sql) as DbOperationResult;
Assert.NotNull(result);
Assert.False(result.Success);
Assert.Contains("UPDATE operation is not allowed in READONLY mode", result.Error ?? string.Empty);
}
finally
{
// Clean up environment variable
Environment.SetEnvironmentVariable("READONLY", null);
}
}

[Fact]
public async Task ReadOnlyMode_DropTable_ReturnsError_WhenReadOnlyIsTrue()
{
// Set READONLY environment variable
Environment.SetEnvironmentVariable("READONLY", "true");
try
{
var sql = $"DROP TABLE IF EXISTS {_tableName}";
var result = await _tools.DropTable(sql) as DbOperationResult;
Assert.NotNull(result);
Assert.False(result.Success);
Assert.Contains("DROP TABLE operation is not allowed in READONLY mode", result.Error ?? string.Empty);
}
finally
{
// Clean up environment variable
Environment.SetEnvironmentVariable("READONLY", null);
}
}

[Fact]
public async Task TestConnection_ReturnsSuccess_WhenConnectionIsValid()
{
var result = await _tools.TestConnection() as DbOperationResult;
Assert.NotNull(result);
Assert.True(result.Success);
Assert.NotNull(result.Data);

var dict = result.Data as System.Collections.IDictionary;
Assert.NotNull(dict);
Assert.True(dict.Contains("ConnectionState"));
Assert.True(dict.Contains("Database"));
Assert.True(dict.Contains("ServerVersion"));
Assert.True(dict.Contains("DataSource"));
Assert.True(dict.Contains("ConnectionTimeout"));
Assert.Equal("Open", dict["ConnectionState"]?.ToString());
}
}
}
29 changes: 25 additions & 4 deletions MssqlMcp/dotnet/MssqlMcp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;

namespace Mssql.McpServer;

Expand All @@ -30,11 +31,31 @@ private static async Task Main(string[] args)
_ = builder.Services.AddSingleton<ISqlConnectionFactory, SqlConnectionFactory>();
_ = builder.Services.AddSingleton<Tools>();

// Register MCP server and tools (instance-based)
_ = builder.Services
// Check if READONLY mode is enabled
bool isReadOnlyMode = Environment.GetEnvironmentVariable("READONLY")?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false;

// Register MCP server and tools
var mcpServerBuilder = builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
.WithStdioServerTransport();

if (isReadOnlyMode)
{
// Load only ReadOnly tools when in ReadOnly mode
var readOnlyTools = new List<McpServerTool>
{
McpServerTool.Create(typeof(Tools).GetMethod("TestConnection")!, typeof(Tools), new() { Services = null }),
McpServerTool.Create(typeof(Tools).GetMethod("DescribeTable")!, typeof(Tools), new() { Services = null }),
McpServerTool.Create(typeof(Tools).GetMethod("ListTables")!, typeof(Tools), new() { Services = null }),
McpServerTool.Create(typeof(Tools).GetMethod("ReadData")!, typeof(Tools), new() { Services = null })
};
mcpServerBuilder.WithTools(readOnlyTools);
}
else
{
// Load all tools from assembly when not in ReadOnly mode
mcpServerBuilder.WithToolsFromAssembly(typeof(Tools).Assembly);
}

// Build the host
var host = builder.Build();
Expand Down
5 changes: 5 additions & 0 deletions MssqlMcp/dotnet/MssqlMcp/Tools/CreateTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public partial class Tools
public async Task<DbOperationResult> CreateTable(
[Description("CREATE TABLE SQL statement")] string sql)
{
if (IsReadOnlyMode)
{
return new DbOperationResult(success: false, error: "CREATE TABLE operation is not allowed in READONLY mode");
}

var conn = await _connectionFactory.GetOpenConnectionAsync();
try
{
Expand Down
5 changes: 5 additions & 0 deletions MssqlMcp/dotnet/MssqlMcp/Tools/DropTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public partial class Tools
public async Task<DbOperationResult> DropTable(
[Description("DROP TABLE SQL statement")] string sql)
{
if (IsReadOnlyMode)
{
return new DbOperationResult(success: false, error: "DROP TABLE operation is not allowed in READONLY mode");
}

var conn = await _connectionFactory.GetOpenConnectionAsync();
try
{
Expand Down
5 changes: 5 additions & 0 deletions MssqlMcp/dotnet/MssqlMcp/Tools/InsertData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public partial class Tools
public async Task<DbOperationResult> InsertData(
[Description("INSERT SQL statement")] string sql)
{
if (IsReadOnlyMode)
{
return new DbOperationResult(success: false, error: "INSERT operation is not allowed in READONLY mode");
}

var conn = await _connectionFactory.GetOpenConnectionAsync();
try
{
Expand Down
Loading