# Directory Structure ``` ├── .github │ └── copilot-instructions.md ├── .gitignore ├── appsettings.json ├── Helpers │ └── SchemaHelper.cs ├── LICENSE ├── MsSqlMCP.csproj ├── MsSqlMCP.sln ├── Program.cs ├── README.md └── SchemaTool.cs ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | # but not Directory.Build.rsp, as it configures directory-level build defaults 86 | !Directory.Build.rsp 87 | *.sbr 88 | *.tlb 89 | *.tli 90 | *.tlh 91 | *.tmp 92 | *.tmp_proj 93 | *_wpftmp.csproj 94 | *.log 95 | *.tlog 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 300 | *.vbp 301 | 302 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 303 | *.dsw 304 | *.dsp 305 | 306 | # Visual Studio 6 technical files 307 | *.ncb 308 | *.aps 309 | 310 | # Visual Studio LightSwitch build output 311 | **/*.HTMLClient/GeneratedArtifacts 312 | **/*.DesktopClient/GeneratedArtifacts 313 | **/*.DesktopClient/ModelManifest.xml 314 | **/*.Server/GeneratedArtifacts 315 | **/*.Server/ModelManifest.xml 316 | _Pvt_Extensions 317 | 318 | # Paket dependency manager 319 | .paket/paket.exe 320 | paket-files/ 321 | 322 | # FAKE - F# Make 323 | .fake/ 324 | 325 | # CodeRush personal settings 326 | .cr/personal 327 | 328 | # Python Tools for Visual Studio (PTVS) 329 | __pycache__/ 330 | *.pyc 331 | 332 | # Cake - Uncomment if you are using it 333 | # tools/** 334 | # !tools/packages.config 335 | 336 | # Tabs Studio 337 | *.tss 338 | 339 | # Telerik's JustMock configuration file 340 | *.jmconfig 341 | 342 | # BizTalk build output 343 | *.btp.cs 344 | *.btm.cs 345 | *.odx.cs 346 | *.xsd.cs 347 | 348 | # OpenCover UI analysis results 349 | OpenCover/ 350 | 351 | # Azure Stream Analytics local run output 352 | ASALocalRun/ 353 | 354 | # MSBuild Binary and Structured Log 355 | *.binlog 356 | 357 | # NVidia Nsight GPU debugger configuration file 358 | *.nvuser 359 | 360 | # MFractors (Xamarin productivity tool) working folder 361 | .mfractor/ 362 | 363 | # Local History for Visual Studio 364 | .localhistory/ 365 | 366 | # Visual Studio History (VSHistory) files 367 | .vshistory/ 368 | 369 | # BeatPulse healthcheck temp database 370 | healthchecksdb 371 | 372 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 373 | MigrationBackup/ 374 | 375 | # Ionide (cross platform F# VS Code tools) working folder 376 | .ionide/ 377 | 378 | # Fody - auto-generated XML schema 379 | FodyWeavers.xsd 380 | 381 | # VS Code files for those working on multiple tools 382 | .vscode/* 383 | !.vscode/settings.json 384 | !.vscode/tasks.json 385 | !.vscode/launch.json 386 | !.vscode/extensions.json 387 | *.code-workspace 388 | 389 | # Local History for Visual Studio Code 390 | .history/ 391 | 392 | # Windows Installer files from build outputs 393 | *.cab 394 | *.msi 395 | *.msix 396 | *.msm 397 | *.msp 398 | 399 | # JetBrains Rider 400 | *.sln.iml 401 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MsSqlMCP 2 | MCP to query SQL Server database schema, such as tables, columns, and relationships 3 | 4 | ## Prerequisites: 5 | - .NET 9 6 | - SQL 7 | 8 | ## Configuration 9 | To configure Copilot in Visual Studio Code, add the MCP server configuration to the `settings.json` file: 10 | 11 | ```json 12 | "mcp": { 13 | "inputs": [], 14 | "servers": { 15 | "MsSqlMCP": { 16 | "type": "stdio", 17 | "command": "dotnet", 18 | "args": [ 19 | "run", 20 | "--project", 21 | "c:\\{path of repository}\\MsSqlMCP\\MsSqlMCP.csproj" 22 | ] 23 | } 24 | } 25 | } 26 | ``` -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Data Source=your_database_server;Initial Catalog=your_database;Encrypt=False;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | } 5 | 6 | } 7 | ``` -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- ```csharp 1 | namespace MsSqlMCP 2 | { 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | 8 | public static class Program 9 | { 10 | public static (string connectionString, bool warning) GetConnectionString() 11 | { 12 | 13 | IConfigurationRoot configuration = new ConfigurationBuilder() 14 | .SetBasePath(AppContext.BaseDirectory) 15 | .AddJsonFile("appsettings.json", optional: false) 16 | .AddEnvironmentVariables() 17 | .Build(); 18 | 19 | string? connectionString = configuration.GetConnectionString("DefaultConnection"); 20 | if (string.IsNullOrEmpty(connectionString)) 21 | { 22 | throw new InvalidOperationException("Connection string 'DefaultConnection' not found in appsettings.json"); 23 | } 24 | 25 | return (connectionString, false); 26 | } 27 | public static async Task Main(string[] args) 28 | { 29 | var builder = Host.CreateApplicationBuilder(args); 30 | builder.Logging.AddConsole(consoleLogOptions => 31 | { 32 | // Configure all logs to go to stderr 33 | consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; 34 | }); 35 | 36 | // Get connection string from appsettings.json to anticipate error 37 | GetConnectionString(); 38 | 39 | builder.Services 40 | .AddMcpServer() 41 | .WithStdioServerTransport() 42 | .WithToolsFromAssembly(); 43 | 44 | 45 | var app = builder.Build(); 46 | 47 | await app.RunAsync(); 48 | } 49 | 50 | } 51 | } 52 | ``` -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- ```markdown 1 | # MsSqlMCP - AI Coding Agent Instructions 2 | 3 | ## Project Overview 4 | MsSqlMCP is a Model Context Protocol (MCP) server that provides SQL Server database schema exploration tools for AI agents. It exposes database metadata through MCP tools that can be called by AI assistants. 5 | 6 | ## Architecture & Key Components 7 | 8 | ### Core Pattern: MCP Tool Registration 9 | - Tools are defined as static methods in `SchemaTool.cs` decorated with `[McpServerTool]` 10 | - Each tool method is automatically registered via `WithToolsFromAssembly()` in `Program.cs` 11 | - Tools follow async pattern: `public async static Task<string> ToolName(params)` 12 | 13 | ### Database Connection Strategy 14 | - Connection string loaded from `appsettings.json` via `Program.GetConnectionString()` 15 | - Always use `using var connection = new SqlConnection(connectionString)` pattern 16 | - Database switching handled via `USE {SanitizeDatabaseName(databaseName)}` commands 17 | - **Critical**: All database names must be sanitized using `SanitizeDatabaseName()` to prevent SQL injection 18 | 19 | ### Helper Layer Pattern 20 | - `SchemaHelper.cs` contains all SQL queries and data access logic 21 | - Tools in `SchemaTool.cs` handle MCP concerns (connection, database switching, formatting) 22 | - Separation: SchemaTool = MCP interface, SchemaHelper = data access 23 | 24 | ## Development Workflows 25 | 26 | ### Adding New MCP Tools 27 | 1. Add method to `SchemaTool.cs` with `[McpServerTool, Description("...")]` 28 | 2. Implement data access method in `SchemaHelper.cs` 29 | 3. Follow connection pattern: open connection, switch database if needed, call helper 30 | 4. Return formatted string (not JSON) - MCP handles serialization 31 | 32 | ### Running & Testing 33 | ```bash 34 | dotnet run --project MsSqlMCP.csproj 35 | ``` 36 | - Configure in VS Code `settings.json` under `"mcp"` section 37 | - Test via Copilot chat which calls the MCP tools 38 | 39 | ### Configuration 40 | - Database connection in `appsettings.json` under `ConnectionStrings.DefaultConnection` 41 | - Supports Windows Authentication (`Trusted_Connection=True`) and SQL auth 42 | - `appsettings.json` copied to output directory via `.csproj` configuration 43 | 44 | ## Code Conventions 45 | 46 | ### SQL Injection Prevention 47 | - **Always** use `SanitizeDatabaseName()` for dynamic database names 48 | - Use parameterized queries (`@parameter`) for user input in SchemaHelper 49 | - Example: `command.Parameters.AddWithValue("@tableName", tableName)` 50 | 51 | ### SQL Query Restrictions 52 | - `ExecuteSql` only allows read-only operations (SELECT, SHOW, DESCRIBE, etc.) 53 | - Blocked statements: DROP, INSERT, UPDATE, DELETE, TRUNCATE, ALTER, CREATE, EXEC, EXECUTE, MERGE, BULK, BACKUP, RESTORE, DBCC, GRANT, DENY, REVOKE, USE, SHUTDOWN, KILL, RECONFIGURE 54 | - Multi-statement queries are validated to prevent restriction bypass 55 | - This ensures the tool remains a safe schema exploration utility 56 | 57 | ### Error Handling Pattern 58 | ```csharp 59 | try { 60 | // SQL operations 61 | } catch (SqlException ex) { 62 | return $"SQL Error: {ex.Message}"; 63 | } catch (Exception ex) { 64 | return $"Error: {ex.Message}"; 65 | } 66 | ``` 67 | 68 | ### Query Result Formatting 69 | - Tables: `schema.tablename` format 70 | - Columns: `name | datatype(length) | NULL/NOT NULL IDENTITY PRIMARY KEY` 71 | - Relationships: `table.column -> referenced_table.referenced_column (FK: name)` 72 | 73 | ## Key Dependencies 74 | - `ModelContextProtocol` (v0.1.0-preview.9): Core MCP framework 75 | - `Microsoft.Data.SqlClient`: SQL Server connectivity 76 | - `Microsoft.Extensions.Hosting`: Dependency injection and configuration 77 | 78 | ## Schema Query Patterns 79 | - Use `INFORMATION_SCHEMA` views for portable metadata queries 80 | - Use `sys.*` views for SQL Server-specific features (foreign keys, procedures) 81 | - Always include schema prefix in table references 82 | - Order results consistently for predictable output 83 | 84 | ## Security Considerations 85 | - All data-modifying statements explicitly blocked in `ExecuteSql`: DROP, INSERT, UPDATE, DELETE, TRUNCATE, ALTER, CREATE, EXEC/EXECUTE, MERGE, BULK, BACKUP, RESTORE, DBCC, GRANT, DENY, REVOKE, USE, SHUTDOWN, KILL, RECONFIGURE 86 | - Multi-statement queries are validated to prevent bypassing restrictions 87 | - Database name sanitization prevents injection 88 | - Connection string supports both Windows and SQL authentication 89 | - Only read-only operations permitted - this is a schema exploration tool only 90 | - No direct file system access - database operations only 91 | ``` -------------------------------------------------------------------------------- /Helpers/SchemaHelper.cs: -------------------------------------------------------------------------------- ```csharp 1 | using Microsoft.Data.SqlClient; 2 | 3 | namespace MsSqlMCP.Helpers 4 | { 5 | public class SchemaHelper 6 | { 7 | 8 | 9 | public async Task<List<string>> GetTablesAsync(SqlConnection connection) 10 | { 11 | var tables = new List<string>(); 12 | 13 | string sql = @" 14 | SELECT TABLE_SCHEMA, TABLE_NAME 15 | FROM INFORMATION_SCHEMA.TABLES 16 | WHERE TABLE_TYPE = 'BASE TABLE' 17 | ORDER BY TABLE_SCHEMA, TABLE_NAME"; 18 | 19 | using var command = new SqlCommand(sql, connection); 20 | using var reader = await command.ExecuteReaderAsync(); 21 | 22 | while (await reader.ReadAsync()) 23 | { 24 | string schema = reader.GetString(0); 25 | string tableName = reader.GetString(1); 26 | tables.Add($"{schema}.{tableName}"); 27 | } 28 | 29 | return tables; 30 | } 31 | 32 | public async Task<List<string>> GetColumnsAsync(SqlConnection connection, string tableName) 33 | { 34 | var columns = new List<string>(); 35 | 36 | string schema = "dbo"; // default value 37 | if (tableName.Contains(".")) 38 | { 39 | var parts = tableName.Split('.'); 40 | schema = parts[0]; 41 | tableName = parts[1]; 42 | } 43 | 44 | string sql = @" 45 | SELECT 46 | COLUMN_NAME, 47 | DATA_TYPE, 48 | CHARACTER_MAXIMUM_LENGTH, 49 | IS_NULLABLE, 50 | COLUMNPROPERTY(object_id(TABLE_SCHEMA + '.' + TABLE_NAME), COLUMN_NAME, 'IsIdentity') as IS_IDENTITY, 51 | ( 52 | SELECT COUNT(*) 53 | FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu 54 | JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc 55 | ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME 56 | WHERE kcu.TABLE_SCHEMA = c.TABLE_SCHEMA 57 | AND kcu.TABLE_NAME = c.TABLE_NAME 58 | AND kcu.COLUMN_NAME = c.COLUMN_NAME 59 | AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY' 60 | ) as IS_PRIMARY_KEY 61 | FROM 62 | INFORMATION_SCHEMA.COLUMNS c 63 | WHERE 64 | TABLE_SCHEMA = @schema 65 | AND TABLE_NAME = @tableName 66 | ORDER BY 67 | ORDINAL_POSITION"; 68 | 69 | using var command = new SqlCommand(sql, connection); 70 | command.Parameters.AddWithValue("@schema", schema); 71 | command.Parameters.AddWithValue("@tableName", tableName); 72 | 73 | using var reader = await command.ExecuteReaderAsync(); 74 | 75 | while (await reader.ReadAsync()) 76 | { 77 | string columnName = reader.GetString(0); 78 | string dataType = reader.GetString(1); 79 | object charMaxLength = reader.GetValue(2); 80 | string isNullable = reader.GetString(3); 81 | int isIdentity = reader.GetInt32(4); 82 | int isPrimaryKey = reader.GetInt32(5); 83 | 84 | string lengthInfo = charMaxLength == DBNull.Value ? "" : $"({charMaxLength})"; 85 | string nullableInfo = isNullable == "YES" ? "NULL" : "NOT NULL"; 86 | string identityInfo = isIdentity == 1 ? "IDENTITY" : ""; 87 | string pkInfo = isPrimaryKey == 1 ? "PRIMARY KEY" : ""; 88 | 89 | columns.Add($"{columnName} | {dataType}{lengthInfo} | {nullableInfo} {identityInfo} {pkInfo}".Trim()); 90 | } 91 | 92 | return columns; 93 | } 94 | 95 | public async Task<List<string>> GetRelationshipsAsync(SqlConnection connection) 96 | { 97 | var relationships = new List<string>(); 98 | 99 | string sql = @" 100 | SELECT 101 | fk.name AS ForeignKey, 102 | OBJECT_NAME(fk.parent_object_id) AS TableName, 103 | COL_NAME(fkc.parent_object_id, fkc.parent_column_id) AS ColumnName, 104 | OBJECT_NAME(fk.referenced_object_id) AS ReferencedTableName, 105 | COL_NAME(fkc.referenced_object_id, fkc.referenced_column_id) AS ReferencedColumnName 106 | FROM 107 | sys.foreign_keys AS fk 108 | INNER JOIN 109 | sys.foreign_key_columns AS fkc 110 | ON fk.OBJECT_ID = fkc.constraint_object_id 111 | ORDER BY 112 | TableName, ReferencedTableName"; 113 | 114 | using var command = new SqlCommand(sql, connection); 115 | using var reader = await command.ExecuteReaderAsync(); 116 | 117 | while (await reader.ReadAsync()) 118 | { 119 | string foreignKey = reader.GetString(0); 120 | string tableName = reader.GetString(1); 121 | string columnName = reader.GetString(2); 122 | string referencedTableName = reader.GetString(3); 123 | string referencedColumnName = reader.GetString(4); 124 | 125 | relationships.Add($"{tableName}.{columnName} -> {referencedTableName}.{referencedColumnName} (FK: {foreignKey})"); 126 | } 127 | 128 | return relationships; 129 | } 130 | 131 | 132 | 133 | public string ExtractTableName(string query) 134 | { 135 | 136 | var words = query.Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); 137 | for (int i = 0; i < words.Length; i++) 138 | { 139 | if ((words[i].ToLower() == "tabla" || words[i].ToLower() == "table") && i + 1 < words.Length) 140 | { 141 | return words[i + 1].Trim(',', '.', ':', ';', '?', '!'); 142 | } 143 | else 144 | { 145 | return words[i].Trim(',', '.', ':', ';', '?', '!'); 146 | } 147 | } 148 | return string.Empty; 149 | } 150 | 151 | public async Task<List<string>> GetStoreProcedureAsync(SqlConnection connection, string spName) 152 | { 153 | var procedures = new List<string>(); 154 | 155 | string sql = @"select name, object_definition(object_id) 156 | from sys.procedures 157 | where name = @spName;"; 158 | 159 | using var command = new SqlCommand(sql, connection); 160 | command.Parameters.AddWithValue("@spName", spName); 161 | using var reader = await command.ExecuteReaderAsync(); 162 | 163 | while (await reader.ReadAsync()) 164 | { 165 | string name = reader.GetString(0); 166 | string definition = reader.IsDBNull(1) ? string.Empty : reader.GetString(1); 167 | procedures.Add($"{name}:\n{definition}"); 168 | } 169 | 170 | return procedures; 171 | } 172 | 173 | } 174 | } 175 | ``` -------------------------------------------------------------------------------- /SchemaTool.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.ComponentModel; 2 | using MsSqlMCP.Helpers; 3 | using ModelContextProtocol.Server; 4 | using Microsoft.Data.SqlClient; 5 | using MsSqlMCP; 6 | using System.Text; 7 | 8 | [McpServerToolType] 9 | public static class SchemaTool 10 | { 11 | // Helper method to sanitize database name to prevent SQL injection. 12 | // For SQL Server, names can be enclosed in []. This also escapes any existing ] characters. 13 | private static string SanitizeDatabaseName(string databaseName) 14 | { 15 | return $"[{databaseName.Replace("]", "]]")}]"; 16 | } 17 | 18 | [McpServerTool, Description("Get tables name of database. Optionally, specify a database name to query a different database in the same instance.")] 19 | public async static Task<string> GetTables(string? databaseName = null) 20 | { 21 | var (connectionString, warning) = Program.GetConnectionString(); 22 | using var connection = new SqlConnection(connectionString); 23 | await connection.OpenAsync(); 24 | 25 | if (!string.IsNullOrWhiteSpace(databaseName)) 26 | { 27 | using var useDbCommand = new SqlCommand($"USE {SanitizeDatabaseName(databaseName)};", connection); 28 | await useDbCommand.ExecuteNonQueryAsync(); 29 | } 30 | 31 | SchemaHelper schemaHelper = new SchemaHelper(); 32 | var tables = await schemaHelper.GetTablesAsync(connection); 33 | string dbInfo = string.IsNullOrWhiteSpace(databaseName) ? string.Empty : $" in database '{databaseName}'"; 34 | return $"Tables{dbInfo}:\n\n{string.Join("\n", tables)}"; 35 | } 36 | 37 | [McpServerTool, Description("Get the columns (fields) of a database table. Optionally, specify a database name to query a different database in the same instance.")] 38 | public async static Task<string> GetColumns(string tableName, string? databaseName = null) 39 | { 40 | var (connectionString, warning) = Program.GetConnectionString(); 41 | using var connection = new SqlConnection(connectionString); 42 | await connection.OpenAsync(); 43 | 44 | if (!string.IsNullOrWhiteSpace(databaseName)) 45 | { 46 | using var useDbCommand = new SqlCommand($"USE {SanitizeDatabaseName(databaseName)};", connection); 47 | await useDbCommand.ExecuteNonQueryAsync(); 48 | } 49 | 50 | SchemaHelper schemaHelper = new SchemaHelper(); 51 | tableName = schemaHelper.ExtractTableName(tableName); 52 | if (!string.IsNullOrEmpty(tableName)) 53 | { 54 | var columns = await schemaHelper.GetColumnsAsync(connection, tableName); 55 | string dbInfo = string.IsNullOrWhiteSpace(databaseName) ? string.Empty : $" (database '{databaseName}')"; 56 | return $"Columns in the table {tableName}{dbInfo}:\n\n{string.Join("\n", columns)}"; 57 | } 58 | else 59 | { 60 | return "Please specify the table name to query its fields."; 61 | } 62 | } 63 | 64 | [McpServerTool, Description("Get the relationships between tables in the database. Optionally, specify a database name to query a different database in the same instance.")] 65 | public async static Task<string> GetRelationships(string? databaseName = null) 66 | { 67 | var (connectionString, warning) = Program.GetConnectionString(); 68 | using var connection = new SqlConnection(connectionString); 69 | await connection.OpenAsync(); 70 | 71 | if (!string.IsNullOrWhiteSpace(databaseName)) 72 | { 73 | using var useDbCommand = new SqlCommand($"USE {SanitizeDatabaseName(databaseName)};", connection); 74 | await useDbCommand.ExecuteNonQueryAsync(); 75 | } 76 | 77 | SchemaHelper schemaHelper = new SchemaHelper(); 78 | var relationships = await schemaHelper.GetRelationshipsAsync(connection); 79 | string dbInfo = string.IsNullOrWhiteSpace(databaseName) ? string.Empty : $" in database '{databaseName}'"; 80 | return $"Relationships between tables{dbInfo}:\n\n{string.Join("\n", relationships)}"; 81 | } 82 | 83 | [McpServerTool, Description("Execute a SQL query against the database. Only allows SELECT statements and other read-only operations. Optionally, specify a database name to query a different database in the same instance.")] 84 | public async static Task<string> ExecuteSql(string sqlQuery, string? databaseName = null) 85 | { 86 | if (string.IsNullOrWhiteSpace(sqlQuery)) 87 | { 88 | return "SQL query cannot be empty."; 89 | } 90 | 91 | // Block any statements that can modify data or structure 92 | string normalizedQuery = sqlQuery.Trim().ToUpperInvariant(); 93 | string[] forbiddenStatements = { 94 | "DROP ", "INSERT ", "UPDATE ", "DELETE ", "TRUNCATE ", "ALTER ", "CREATE ", 95 | "EXEC ", "EXECUTE ", "MERGE ", "BULK ", "BACKUP ", "RESTORE ", "DBCC ", 96 | "GRANT ", "DENY ", "REVOKE ", "USE ", "SHUTDOWN ", "KILL ", "RECONFIGURE " 97 | }; 98 | 99 | foreach (string forbidden in forbiddenStatements) 100 | { 101 | if (normalizedQuery.StartsWith(forbidden)) 102 | { 103 | return $"Error: {forbidden.Trim()} statements are not allowed. Only read-only operations are permitted."; 104 | } 105 | } 106 | 107 | // Additional check for multi-statement queries that might contain forbidden operations 108 | if (normalizedQuery.Contains(";")) 109 | { 110 | string[] statements = sqlQuery.Split(';', StringSplitOptions.RemoveEmptyEntries); 111 | foreach (string statement in statements) 112 | { 113 | string normalizedStatement = statement.Trim().ToUpperInvariant(); 114 | if (string.IsNullOrWhiteSpace(normalizedStatement)) continue; 115 | 116 | foreach (string forbidden in forbiddenStatements) 117 | { 118 | if (normalizedStatement.StartsWith(forbidden)) 119 | { 120 | return $"Error: {forbidden.Trim()} statements are not allowed in multi-statement queries. Only read-only operations are permitted."; 121 | } 122 | } 123 | } 124 | } 125 | 126 | var (connectionString, warningFlag) = Program.GetConnectionString(); 127 | if (warningFlag) 128 | { 129 | // Potentially log the warning or handle it as needed 130 | } 131 | 132 | StringBuilder resultBuilder = new StringBuilder(); 133 | 134 | try 135 | { 136 | using var connection = new SqlConnection(connectionString); 137 | await connection.OpenAsync(); 138 | 139 | if (!string.IsNullOrWhiteSpace(databaseName)) 140 | { 141 | using var useDbCommand = new SqlCommand($"USE {SanitizeDatabaseName(databaseName)};", connection); 142 | await useDbCommand.ExecuteNonQueryAsync(); 143 | resultBuilder.AppendLine($"Switched to database '{SanitizeDatabaseName(databaseName)}'."); // Show sanitized name for clarity 144 | } 145 | 146 | using var command = new SqlCommand(sqlQuery, connection); 147 | bool isSelectQuery = sqlQuery.Trim().ToUpperInvariant().StartsWith("SELECT"); 148 | 149 | if (isSelectQuery) 150 | { 151 | using var reader = await command.ExecuteReaderAsync(); 152 | if (reader.HasRows) 153 | { 154 | for (int i = 0; i < reader.FieldCount; i++) 155 | { 156 | resultBuilder.Append(reader.GetName(i)); 157 | if (i < reader.FieldCount - 1) resultBuilder.Append("\t|\t"); 158 | } 159 | resultBuilder.AppendLine(); 160 | // Ensure separator line is not drawn if resultBuilder is empty or too short 161 | if (resultBuilder.Length > 2) 162 | { 163 | resultBuilder.AppendLine(new string('-', resultBuilder.Length - (resultBuilder.ToString().EndsWith(Environment.NewLine) ? Environment.NewLine.Length*2 : Environment.NewLine.Length) )); 164 | } else if (resultBuilder.Length > 0 && !resultBuilder.ToString().EndsWith(Environment.NewLine)) { 165 | resultBuilder.AppendLine(new string('-', resultBuilder.Length)); 166 | } 167 | 168 | while (await reader.ReadAsync()) 169 | { 170 | for (int i = 0; i < reader.FieldCount; i++) 171 | { 172 | resultBuilder.Append(reader[i]?.ToString() ?? "NULL"); 173 | if (i < reader.FieldCount - 1) resultBuilder.Append("\t|\t"); 174 | } 175 | resultBuilder.AppendLine(); 176 | } 177 | } 178 | else 179 | { 180 | resultBuilder.AppendLine("No rows returned from the query."); 181 | } 182 | } 183 | else 184 | { 185 | int affectedRows = await command.ExecuteNonQueryAsync(); 186 | resultBuilder.AppendLine($"Command executed successfully. {affectedRows} row(s) affected."); 187 | } 188 | } 189 | catch (SqlException ex) 190 | { 191 | return $"SQL Error: {ex.Message}"; 192 | } 193 | catch (Exception ex) 194 | { 195 | return $"Error executing SQL query: {ex.Message}"; 196 | } 197 | 198 | return resultBuilder.ToString(); 199 | } 200 | 201 | [McpServerTool, Description("Get the definition of a stored procedure by name. Optionally, specify a database name to query a different database in the same instance.")] 202 | public async static Task<string> GetStoreProcedure(string spName, string? databaseName = null) 203 | { 204 | if (string.IsNullOrWhiteSpace(spName)) 205 | { 206 | return "Stored procedure name cannot be empty."; 207 | } 208 | 209 | var (connectionString, warning) = Program.GetConnectionString(); 210 | using var connection = new SqlConnection(connectionString); 211 | await connection.OpenAsync(); 212 | 213 | if (!string.IsNullOrWhiteSpace(databaseName)) 214 | { 215 | using var useDbCommand = new SqlCommand($"USE {SanitizeDatabaseName(databaseName)};", connection); 216 | await useDbCommand.ExecuteNonQueryAsync(); 217 | } 218 | 219 | SchemaHelper schemaHelper = new SchemaHelper(); 220 | var procedures = await schemaHelper.GetStoreProcedureAsync(connection, spName); 221 | string dbInfo = string.IsNullOrWhiteSpace(databaseName) ? string.Empty : $" in database '{databaseName}'"; 222 | if (procedures.Count == 0) 223 | { 224 | return $"Stored procedure '{spName}' not found{dbInfo}."; 225 | } 226 | return $"Stored procedure '{spName}'{dbInfo}:\n\n{string.Join("\n\n", procedures)}"; 227 | } 228 | } 229 | ```