diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc.slnx b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc.slnx new file mode 100644 index 0000000..707a0b3 --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc.slnx @@ -0,0 +1,3 @@ + + + diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/.gitignore b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/.gitignore new file mode 100644 index 0000000..ad9b6e1 --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +# local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/DbOperationResult.cs b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/DbOperationResult.cs new file mode 100644 index 0000000..d3aefa8 --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/DbOperationResult.cs @@ -0,0 +1,24 @@ +namespace MssqlMcpAzFunc; + +public class DbOperationResult(bool success, string? error = null, int? rowsAffected = null, object? data = null) +{ + /// + /// Gets a value indicating whether the database operation was successful. + /// + public bool Success { get; } = success; + + /// + /// Gets the error message if the operation failed; otherwise, null. + /// + public string? Error { get; } = error; + + /// + /// Gets the number of rows affected by the operation, if applicable. + /// + public int? RowsAffected { get; } = rowsAffected; + + /// + /// Gets any data returned by the operation, such as query results. + /// + public object? Data { get; } = data; +} diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Functions/Tools.cs b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Functions/Tools.cs new file mode 100644 index 0000000..7eb5d09 --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Functions/Tools.cs @@ -0,0 +1,382 @@ +using System.Data; +using System.Text.Json; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Extensions.Mcp; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; + +namespace MssqlMcpAzFunc.Functions; + +public class Tools(ILogger logger, ISqlConnectionFactory connectionFactory) +{ + private readonly ILogger _logger = logger; + private readonly ISqlConnectionFactory _connectionFactory = connectionFactory; + + // Helper to convert DataTable to a serializable list + private static List> DataTableToList(DataTable table) + { + var result = new List>(); + foreach (DataRow row in table.Rows) + { + var dict = new Dictionary(); + foreach (DataColumn col in table.Columns) + { + dict[col.ColumnName] = row[col]; + } + result.Add(dict); + } + return result; + } + + [Function(nameof(CreateTable))] + public async Task CreateTable( + [McpToolTrigger("create_table", "Creates a new table in the SQL Database. Expects a valid CREATE TABLE SQL statement as input.")] ToolInvocationContext context, + [McpToolProperty("sql", "CREATE TABLE SQL statement")] string sql) + { + var conn = await _connectionFactory.GetOpenConnectionAsync(); + try + { + using (conn) + { + using var cmd = new SqlCommand(sql, conn); + _ = await cmd.ExecuteNonQueryAsync(); + return JsonSerializer.Serialize(new DbOperationResult(success: true)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "CreateTable failed: {Message}", ex.Message); + return JsonSerializer.Serialize(new DbOperationResult(success: false, error: ex.Message)); + } + } + + [Function("DescribeTable")] + public async Task DescribeTable( + [McpToolTrigger("describe_table", "Returns table schema.")] ToolInvocationContext context, + [McpToolProperty("name", "Name of table")] string name) + { + string? schema = null; + if (name.Contains('.')) + { + // If the table name contains a schema, split it into schema and table name + var parts = name.Split('.'); + if (parts.Length > 1) + { + name = parts[1]; // Use only the table name part + schema = parts[0]; // Use the first part as schema + } + } + // Query for table metadata + const string TableInfoQuery = @"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 = @TableName and (s.name = @TableSchema or @TableSchema IS NULL) "; + + // Query for columns + const string ColumnsQuery = @"SELECT c.name, ty.name AS type, c.max_length AS length, c.precision, c.scale, 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 = @TableName and (s.name = @TableSchema or @TableSchema IS NULL ) )"; + + // Query for indexes + const string IndexesQuery = @"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 = @TableName and (s.name = @TableSchema or @TableSchema IS NULL ) ) AND i.is_primary_key = 0 AND i.is_unique_constraint = 0"; + + // Query for constraints + const string ConstraintsQuery = @"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 = @TableName and (s.name = @TableSchema or @TableSchema IS NULL ) )"; + + + const string ForeignKeyInformation = @"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) = @TableSchema OR @TableSchema IS NULL ) + AND tp.name = @TableName +GROUP BY + fk.name, tp.schema_id, tp.name, tr.schema_id, tr.name; +"; + var conn = await _connectionFactory.GetOpenConnectionAsync(); + try + { + using (conn) + { + var result = new Dictionary(); + // Table info + using (var cmd = new SqlCommand(TableInfoQuery, conn)) + { + var _ = cmd.Parameters.AddWithValue("@TableName", name); + _ = cmd.Parameters.AddWithValue("@TableSchema", schema == null ? DBNull.Value : schema); + using var reader = await cmd.ExecuteReaderAsync(); + if (await reader.ReadAsync()) + { + result["table"] = new + { + id = reader["id"], + name = reader["name"], + schema = reader["schema"], + owner = reader["owner"], + type = reader["type"], + description = reader["description"] is DBNull ? null : reader["description"] + }; + } + else + { + return JsonSerializer.Serialize(new DbOperationResult(success: false, error: $"Table '{name}' not found.")); + } + } + // Columns + using (var cmd = new SqlCommand(ColumnsQuery, conn)) + { + var _ = cmd.Parameters.AddWithValue("@TableName", name); + _ = cmd.Parameters.AddWithValue("@TableSchema", schema == null ? DBNull.Value : schema); + using var reader = await cmd.ExecuteReaderAsync(); + var columns = new List(); + while (await reader.ReadAsync()) + { + columns.Add(new + { + name = reader["name"], + type = reader["type"], + length = reader["length"], + precision = reader["precision"], + scale = reader["scale"], + nullable = (bool)reader["nullable"], + description = reader["description"] is DBNull ? null : reader["description"] + }); + } + result["columns"] = columns; + } + // Indexes + using (var cmd = new SqlCommand(IndexesQuery, conn)) + { + var _ = cmd.Parameters.AddWithValue("@TableName", name); + _ = cmd.Parameters.AddWithValue("@TableSchema", schema == null ? DBNull.Value : schema); + using var reader = await cmd.ExecuteReaderAsync(); + var indexes = new List(); + while (await reader.ReadAsync()) + { + indexes.Add(new + { + name = reader["name"], + type = reader["type"], + description = reader["description"] is DBNull ? null : reader["description"], + keys = reader["keys"] + }); + } + result["indexes"] = indexes; + } + // Constraints + using (var cmd = new SqlCommand(ConstraintsQuery, conn)) + { + var _ = cmd.Parameters.AddWithValue("@TableName", name); + _ = cmd.Parameters.AddWithValue("@TableSchema", schema == null ? DBNull.Value : schema); + using var reader = await cmd.ExecuteReaderAsync(); + var constraints = new List(); + while (await reader.ReadAsync()) + { + constraints.Add(new + { + name = reader["name"], + type = reader["type"], + keys = reader["keys"] + }); + } + result["constraints"] = constraints; + } + + // Foreign Keys + using (var cmd = new SqlCommand(ForeignKeyInformation, conn)) + { + var _ = cmd.Parameters.AddWithValue("@TableName", name); + _ = cmd.Parameters.AddWithValue("@TableSchema", schema == null ? DBNull.Value : schema); + using var reader = await cmd.ExecuteReaderAsync(); + var foreignKeys = new List(); + while (await reader.ReadAsync()) + { + foreignKeys.Add(new + { + name = reader["name"], + schema = reader["schema"], + table_name = reader["table_name"], + column_name = reader["column_names"], + referenced_schema = reader["referenced_schema"], + referenced_table = reader["referenced_table"], + referenced_column = reader["referenced_column_names"], + }); + } + result["foreignKeys"] = foreignKeys; + } + + return JsonSerializer.Serialize(new DbOperationResult(success: true, data: result)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "DescribeTable failed: {Message}", ex.Message); + return JsonSerializer.Serialize(new DbOperationResult(success: false, error: ex.Message)); + } + } + + [Function("DropTable")] + public async Task DropTable( + [McpToolTrigger("drop_table", "Drops a table in the SQL Database. Expects a valid DROP TABLE SQL statement as input.")] ToolInvocationContext context, + [McpToolProperty("sql", "DROP TABLE SQL statement")] string sql) + { + var conn = await _connectionFactory.GetOpenConnectionAsync(); + try + { + using (conn) + { + using var cmd = new SqlCommand(sql, conn); + _ = await cmd.ExecuteNonQueryAsync(); + return JsonSerializer.Serialize(new DbOperationResult(success: true)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "DropTable failed: {Message}", ex.Message); + return JsonSerializer.Serialize(new DbOperationResult(success: false, error: ex.Message)); + } + } + + [Function("InsertData")] + public async Task InsertData( + [McpToolTrigger("insert_data", "Inserts data into a table in the SQL Database. Expects a valid INSERT SQL statement as input.")] ToolInvocationContext context, + [McpToolProperty("sql", "INSERT SQL statement")] string sql) + { + var conn = await _connectionFactory.GetOpenConnectionAsync(); + try + { + using (conn) + { + using var cmd = new SqlCommand(sql, conn); + var rows = await cmd.ExecuteNonQueryAsync(); + return JsonSerializer.Serialize(new DbOperationResult(success: true, rowsAffected: rows)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "InsertData failed: {Message}", ex.Message); + return JsonSerializer.Serialize(new DbOperationResult(success: false, error: ex.Message)); + } + } + + [Function("ListTables")] + public async Task ListTables( + [McpToolTrigger("list_tables", "Lists all tables in the SQL Database.")] ToolInvocationContext context) + { + _logger.LogWarning(context?.Transport?.Name); + var tmp = ((HttpTransport)(context?.Transport)).Headers; + _logger.LogWarning("Headers: {Headers}", string.Join(", ", tmp.Select(kv => $"{kv.Key}: {string.Join(";", kv.Value)}"))); + _logger.LogWarning(((HttpTransport)(context?.Transport)).Headers.FirstOrDefault(h => h.Key == "testing-header").Value); + + + string ListTablesQuery = @"SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_SCHEMA, TABLE_NAME"; + + var conn = await _connectionFactory.GetOpenConnectionAsync(); + try + { + using (conn) + { + using var cmd = new SqlCommand(ListTablesQuery, conn); + var tables = new List(); + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + tables.Add($"{reader.GetString(0)}.{reader.GetString(1)}"); + } + return JsonSerializer.Serialize(new DbOperationResult(success: true, data: tables)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "ListTables failed: {Message}", ex.Message); + return JsonSerializer.Serialize(new DbOperationResult(success: false, error: ex.Message)); + } + } + + [Function("ReadData")] + public async Task ReadData( + [McpToolTrigger("read_data", "Executes SQL queries against SQL Database to read data")] ToolInvocationContext context, + [McpToolProperty("sql", "SQL query to execute")] string sql) + { + var conn = await _connectionFactory.GetOpenConnectionAsync(); + try + { + using (conn) + { + using var cmd = new SqlCommand(sql, conn); + using var reader = await cmd.ExecuteReaderAsync(); + var results = new List>(); + while (await reader.ReadAsync()) + { + var row = new Dictionary(); + for (var i = 0; i < reader.FieldCount; i++) + { + row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i); + } + results.Add(row); + } + return JsonSerializer.Serialize(new DbOperationResult(success: true, data: results)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "ReadData failed: {Message}", ex.Message); + return JsonSerializer.Serialize(new DbOperationResult(success: false, error: ex.Message)); + } + } + + [Function("UpdateData")] + public async Task UpdateData( + [McpToolTrigger("update_data", "Updates data in a table in the SQL Database. Expects a valid UPDATE SQL statement as input.")] ToolInvocationContext context, + [McpToolProperty("sql", "UPDATE SQL statement")] string sql) + { + var conn = await _connectionFactory.GetOpenConnectionAsync(); + try + { + using (conn) + { + using var cmd = new SqlCommand(sql, conn); + var rows = await cmd.ExecuteNonQueryAsync(); + return JsonSerializer.Serialize(new DbOperationResult(true, null, rows)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "UpdateData failed: {Message}", ex.Message); + return JsonSerializer.Serialize(new DbOperationResult(false, ex.Message)); + } + + } +} diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/ISqlConnectionFactory.cs b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/ISqlConnectionFactory.cs new file mode 100644 index 0000000..eeaa50f --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/ISqlConnectionFactory.cs @@ -0,0 +1,9 @@ +using Microsoft.Data.SqlClient; + +/// +/// Defines a factory interface for creating SQL database connections. +/// +public interface ISqlConnectionFactory +{ + Task GetOpenConnectionAsync(); +} \ No newline at end of file diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/MssqlMcpAzFunc.csproj b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/MssqlMcpAzFunc.csproj new file mode 100644 index 0000000..24d0dad --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/MssqlMcpAzFunc.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + v4 + Exe + enable + enable + + + + + + + + + + + + + + diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Program.cs b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Program.cs new file mode 100644 index 0000000..704475c --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Program.cs @@ -0,0 +1,14 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = FunctionsApplication.CreateBuilder(args); + +builder.ConfigureFunctionsWebApplication(); + +builder.Services + .AddApplicationInsightsTelemetryWorkerService() + .ConfigureFunctionsApplicationInsights(); + +builder.Build().Run(); diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Properties/launchSettings.json b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Properties/launchSettings.json new file mode 100644 index 0000000..9fa3baa --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "MssqlMcpAzFunc": { + "commandName": "Project", + "commandLineArgs": "--port 7277", + "launchBrowser": false + } + } +} \ No newline at end of file diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Properties/serviceDependencies.json b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Properties/serviceDependencies.json new file mode 100644 index 0000000..df4dcc9 --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Properties/serviceDependencies.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "appInsights1": { + "type": "appInsights" + }, + "storage1": { + "type": "storage", + "connectionId": "AzureWebJobsStorage" + } + } +} \ No newline at end of file diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Properties/serviceDependencies.local.json b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Properties/serviceDependencies.local.json new file mode 100644 index 0000000..b804a28 --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/Properties/serviceDependencies.local.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "appInsights1": { + "type": "appInsights.sdk" + }, + "storage1": { + "type": "storage.emulator", + "connectionId": "AzureWebJobsStorage" + } + } +} \ No newline at end of file diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/SqlConnectionFactory.cs b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/SqlConnectionFactory.cs new file mode 100644 index 0000000..2412a9c --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/SqlConnectionFactory.cs @@ -0,0 +1,25 @@ +using Microsoft.Data.SqlClient; + +namespace MssqlMcpAzFunc; + +public class SqlConnectionFactory : ISqlConnectionFactory +{ + public async Task GetOpenConnectionAsync() + { + var connectionString = GetConnectionString(); + + // Let ADO.Net handle connection pooling + var conn = new SqlConnection(connectionString); + await conn.OpenAsync(); + return conn; + } + + private static string GetConnectionString() + { + var connectionString = Environment.GetEnvironmentVariable("CONNECTION_STRING"); + + return string.IsNullOrEmpty(connectionString) + ? throw new InvalidOperationException("Connection string is not set in the environment variable 'CONNECTION_STRING'.\n\nHINT: Have a local SQL Server, with a database called 'test', from console, run `SET CONNECTION_STRING=Server=.;Database=test;Trusted_Connection=True;TrustServerCertificate=True` and the load the .sln file") + : connectionString; + } +} diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/host.json b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/host.json new file mode 100644 index 0000000..ee5cf5f --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/host.json @@ -0,0 +1,12 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + } +} \ No newline at end of file diff --git a/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/local.settings.json b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/local.settings.json new file mode 100644 index 0000000..64a872f --- /dev/null +++ b/MssqlMcp/AzureFunctions/MssqlMcpAzFunc/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" + } +} \ No newline at end of file