diff --git a/src/Dax.Formatter.McpServer/Dax.Formatter.McpServer.csproj b/src/Dax.Formatter.McpServer/Dax.Formatter.McpServer.csproj new file mode 100644 index 0000000..9375c25 --- /dev/null +++ b/src/Dax.Formatter.McpServer/Dax.Formatter.McpServer.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + latest + enable + enable + Dax.Formatter.McpServer + Dax.Formatter.McpServer + false + + + + + + + + + + + + diff --git a/src/Dax.Formatter.McpServer/FormatDaxTool.cs b/src/Dax.Formatter.McpServer/FormatDaxTool.cs new file mode 100644 index 0000000..6e4f2b1 --- /dev/null +++ b/src/Dax.Formatter.McpServer/FormatDaxTool.cs @@ -0,0 +1,84 @@ +namespace Dax.Formatter.McpServer; + +using Dax.Formatter; +using Dax.Formatter.Models; +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.Reflection; + +[McpServerToolType] +internal static class FormatDaxTool +{ + [McpServerTool( + Name = "format_dax", + Title = "Format DAX expressions", + ReadOnly = true, + Idempotent = true, + OpenWorld = true, + Destructive = false)] + [Description(""" + Formats DAX (Data Analysis Expressions) code. DAX comments are preserved. Supports both DAX queries and DAX expressions. + + WHEN TO CALL: + - The user asks to format, beautify, prettify or normalize DAX code. + + RETURNS: + An array of responses, one per input in the same order: + { + Formatted: string, + Errors: [{ Line: int, Column: int, Message: string }] + } + A format failure (invalid DAX) results in null `Formatted` and a non-empty `Errors` array. + A successful format results in a non-null `Formatted` and an empty or null `Errors` array. + + NOTE: + This tool calls an external HTTP service. Always batch multiple snippets + in a single call — looping the tool wastes round-trips and is rate-limited upstream. + """)] + public static async Task FormatDax( + IDaxFormatterClient client, + [Description(""" + The DAX expressions to format. Each array element must be a complete, + independent piece of DAX code — either a DAX query or a DAX expression. + """)] + string[] expressions, + [Description("List separator character.")] + char listSeparator = ',', + [Description("Decimal separator character.")] + char decimalSeparator = '.', + [Description(""" + Controls how arguments and sub-expressions are wrapped across lines. + - 'LongLine': keeps arguments compact horizontally where readable. + - 'ShortLine': breaks each argument onto its own line for maximum vertical clarity. + """)] + DaxFormatterLineStyle lineStyle = DaxFormatterLineStyle.LongLine, + [Description($""" + Controls spacing between function names and their opening parentheses. + - 'SpaceAfterFunction': adds a space for readability, e.g. 'SUM (x)'. + - 'NoSpaceAfterFunction': removes the space for compactness, e.g. 'SUM(x)'. + """)] + DaxFormatterSpacingStyle spacingStyle = DaxFormatterSpacingStyle.SpaceAfterFunction, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(expressions); + + if (expressions.Length == 0) + return []; + + var request = new DaxFormatterMultipleRequest(); + { + // Set formatting options + request.DecimalSeparator = decimalSeparator; + request.ListSeparator = listSeparator; + request.MaxLineLength = lineStyle; + request.SkipSpaceAfterFunctionName = spacingStyle; + // Add caller info + request.CallerApp = "Dax.Formatter.McpServer"; + request.CallerVersion = typeof(Program).Assembly.GetCustomAttribute()?.InformationalVersion; + } + request.Dax.AddRange(expressions); + + var responses = await client.FormatAsync(request, cancellationToken).ConfigureAwait(false); + return [.. responses]; + } +} diff --git a/src/Dax.Formatter.McpServer/Program.cs b/src/Dax.Formatter.McpServer/Program.cs new file mode 100644 index 0000000..565772b --- /dev/null +++ b/src/Dax.Formatter.McpServer/Program.cs @@ -0,0 +1,57 @@ +using Dax.Formatter; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Reflection; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(options => options.LogToStandardErrorThreshold = LogLevel.Trace); + +builder.Services.AddSingleton(); +builder.Services + .AddMcpServer(ConfigureMcpServerOptions) + .WithStdioServerTransport() + .WithToolsFromAssembly(); + +await builder.Build().RunAsync(); + +static void ConfigureMcpServerOptions(ModelContextProtocol.Server.McpServerOptions options) +{ + options.ServerInfo = new ModelContextProtocol.Protocol.Implementation + { + Name = "dax-formatter-mcp", + Version = typeof(Program).Assembly.GetCustomAttribute()?.InformationalVersion ?? "0.0.0", + WebsiteUrl = "https://www.daxformatter.com" + }; + options.ServerInstructions = """ + # DAX Formatter MCP Server + + ## Core Purpose + + This server formats DAX (Data Analysis Expressions) source code for Microsoft + Power BI, Analysis Services, and Tabular models. It is the canonical wrapper + for the SQLBI daxformatter.com web service. + + ## Strict Behavioral Rules + + ### 1. Tool Selection + + - No bypass: when DAX needs formatting, you MUST use this server's tools. + NEVER call the daxformatter.com HTTP endpoint directly. + This server is the single canonical channel for DAX formatting. + + ### 2. Code Integrity + + - Pass input as-is: the user's DAX is the authoritative source. You MUST + send it to the tool exactly as provided without altering the string in any way. + You MAY propose or apply changes only when the user has explicitly asked you to do so. + + ### 3. Surface Errors + + - Relay verbatim: if the formatter reports errors, you MUST pass them to + the user exactly as received including all details. They are diagnostic + information the user needs to fix the code. + """; +} \ No newline at end of file diff --git a/src/Dax.Formatter.sln b/src/Dax.Formatter.sln index 714797f..e14b88c 100644 --- a/src/Dax.Formatter.sln +++ b/src/Dax.Formatter.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dax.Formatter", "Dax.Format EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dax.Formatter.Tests", "Dax.Formatter.Tests\Dax.Formatter.Tests.csproj", "{44162C15-9AC7-406F-B1A4-1A49E73F962B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dax.Formatter.McpServer", "Dax.Formatter.McpServer\Dax.Formatter.McpServer.csproj", "{D839ACC9-427A-40AC-81FD-1DC4352FA019}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {44162C15-9AC7-406F-B1A4-1A49E73F962B}.Debug|Any CPU.Build.0 = Debug|Any CPU {44162C15-9AC7-406F-B1A4-1A49E73F962B}.Release|Any CPU.ActiveCfg = Release|Any CPU {44162C15-9AC7-406F-B1A4-1A49E73F962B}.Release|Any CPU.Build.0 = Release|Any CPU + {D839ACC9-427A-40AC-81FD-1DC4352FA019}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D839ACC9-427A-40AC-81FD-1DC4352FA019}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D839ACC9-427A-40AC-81FD-1DC4352FA019}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D839ACC9-427A-40AC-81FD-1DC4352FA019}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE