Skip to content
Merged
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
72 changes: 60 additions & 12 deletions mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,86 @@

A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that formats and validates DAX from any MCP-enabled AI client — Claude Code, Claude Desktop, Cursor, VS Code, and others — using the SQLBI DAX Formatter service.

Local **stdio** server, run on demand via `npx`. Requires [Node.js](https://nodejs.org) 18+.

## Installation guide

The server is **local, stdio, anonymous** — no install step, no API key, no login. It exposes one tool, `format_dax`. After connecting, just ask the agent to format DAX.
The server runs **locally on your machine** — no install step, no account, no API key, no login. It exposes a single tool, `format_dax`. Find your app below and follow the short steps; afterwards, just ask your assistant to *"format this DAX"*.

### Visual Studio Code

[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_DAX_Formatter-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect/mcp/install?name=DaxFormatter&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40sqlbi%2Fdaxformatter-mcp%22%5D%7D)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_DAX_Formatter-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=DaxFormatter&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40sqlbi%2Fdaxformatter-mcp%22%5D%7D&quality=insiders)

Click a badge to install (VS Code asks for confirmation), or add it from the command line:
Click a badge above — VS Code opens and asks you to confirm. That's it.

<details>
<summary>Prefer the terminal?</summary>

```bash
code --add-mcp '{"name":"DaxFormatter","type":"stdio","command":"npx","args":["-y","@sqlbi/daxformatter-mcp"]}'
code --add-mcp "{\"name\":\"DaxFormatter\",\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"-y\",\"@sqlbi/daxformatter-mcp\"]}"
```
</details>

### Claude

**Claude Desktop** — add it through the config file:

1. Open **Settings → Developer → Edit Config** to reveal `claude_desktop_config.json`.
2. Inside `mcpServers`, add the standard `DaxFormatter` entry shown in [Any MCP client](#any-mcp-client) below.
3. Restart Claude Desktop, then confirm the server appears under **Settings → Developer**.

The `format_dax` tool is now available in chat. The same connector is automatically shared with Claude Code, so you set it up once for both.

### Claude Code (CLI)
<details>
<summary>Advanced: add it from the Claude Code terminal</summary>

```bash
# add the server (-s user enables it in every project; omit -s for the current project only)
# -s user enables it in every project; omit -s for the current project only
claude mcp add -s user DaxFormatter -- npx -y @sqlbi/daxformatter-mcp

# verify it was added and check the connection status
# check it was added
claude mcp get DaxFormatter
```
</details>

### Claude Desktop / other clients (config file)
### Codex

Add this to the client's MCP configuration file (e.g. `claude_desktop_config.json`, or a `.mcp.json` at the repo root):
In the **Codex app**:

1. Open **Settings → Integrations → MCP servers → Add**.
2. Choose the **command (STDIO)** type and give it a name (e.g. `DaxFormatter`).
3. Set the command to `npx`, then use **Add argument** to add the two arguments separately: first `-y`, then `@sqlbi/daxformatter-mcp`.
4. Click **Save**.

The `format_dax` tool is now available in your threads. The app and the CLI share the same settings, so this also covers the **Codex CLI** and IDE extension.

<details>
<summary>Advanced: add it from the Codex terminal, or edit the config file by hand</summary>

Add it from the terminal:

```bash
# add it to the user-level Codex configuration (available in every project)
codex mcp add DaxFormatter -- npx -y @sqlbi/daxformatter-mcp

# check it was added
codex mcp get DaxFormatter
codex mcp list
```

Both the app and the CLI write the same entry to `~/.codex/config.toml` (Windows: `%USERPROFILE%\.codex\config.toml`), which you can also edit by hand:

```toml
[mcp_servers.DaxFormatter]
command = "npx"
args = ["-y", "@sqlbi/daxformatter-mcp"]
```

To scope the server to one repository, put the same table in a project-scoped `.codex/config.toml` at the repo root instead. Codex loads it only for **trusted** projects and prompts for trust on first use.
</details>

### Any MCP client

Don't see your app above? Most MCP-aware tools read a standard JSON config. Add this entry to your client's configuration file — or to a `.mcp.json` at the root of a project to share it with everyone working on that repo:

```json
{
Expand All @@ -44,8 +94,6 @@ Add this to the client's MCP configuration file (e.g. `claude_desktop_config.jso
}
```

No separate install step: `npx` downloads and runs the server on demand.

## What it does

The server exposes a single tool, **`format_dax`** — just ask your assistant to format or check some DAX and it will use it:
Expand Down
47 changes: 40 additions & 7 deletions mcp/src/format-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,75 @@ import { CALLER_APP } from "./version";

export const FORMAT_DAX_TOOL_NAME = "format_dax";

export const FORMAT_DAX_TOOL_DESCRIPTION = `Format and validate DAX (Data Analysis Expressions) using the SQLBI DAX Formatter service.
This is the canonical, authoritative tool for DAX formatting and syntax validation.

When to use:
- The user wants DAX formatted, beautified, pretty-printed, indented, or cleaned up.
- The user wants to check whether DAX is syntactically valid.

How to call:
- Batch all expressions into a SINGLE call via \`expressions\`.
- Do not call the tool once per expression.

Hard rules:
- Never format, fix, rewrite, or validate DAX yourself.
- Never call daxformatter.com directly. Always use this tool.
- Do not invent, repair, or modify the returned formatted text.

Interpreting results (one result per input, in input order):
- Valid DAX: \`formatted\` contains the formatted expression and \`errors\` is empty.
- Invalid DAX: \`formatted\` is null and \`errors\` contains parser errors.
- Parser errors are normal results, not tool failures.

Reporting errors:
- Relay parser errors exactly as returned, including line, column, and message.
- Report service/network failures separately from DAX syntax errors.`;

const inputSchema = z.object({
expressions: z
.array(z.string().min(1))
.min(1)
.describe(
"One or more DAX expressions to format. Several are formatted in a single request.",
'DAX expressions to format. ALWAYS provide an array, even for a single expression: ["SUM(Sales[Amount])"]. Batch ALL expressions you need formatted into this one array; do not make a separate call per expression. Results are returned in the same order.',
),
lineStyle: z
.enum(["longLine", "shortLine"])
.optional()
.describe("Line-length style. Defaults to longLine."),
.describe(
"Line breaking style. 'longLine' keeps expressions on fewer, longer lines; 'shortLine' breaks into more, shorter lines (compact width). Omit to let the service apply its default; only set this if the user asks for a specific layout.",
),
spacingStyle: z
.enum(["spaceAfterFunction", "noSpaceAfterFunction"])
.optional()
.describe("Spacing after a function name. Defaults to spaceAfterFunction."),
.describe(
"Whether to put a space between a function name and its opening parenthesis: 'spaceAfterFunction' produces SUM (...), 'noSpaceAfterFunction' produces SUM(...). Omit to let the service apply its default; only set this if the user asks.",
),
listSeparator: z
.string()
.length(1)
.optional()
.describe("List separator character. Defaults to ','."),
.describe(
"Character separating function arguments and list items. Omit to let the service apply its default.",
),
decimalSeparator: z
.string()
.length(1)
.optional()
.describe("Decimal separator character. Defaults to '.'."),
.describe(
"Character for the decimal point in numbers. Omit to let the service apply its default.",
),
serverName: z
.string()
.optional()
.describe(
"Name of the server the DAX expressions were taken from. Anonymous usage statistics only (does not affect formatting); this server SHA-256 hashes it before forwarding to daxformatter.com, so the statistics service only ever sees the hash. Pass it only if you can retrieve a real value (e.g. from the active connection or the model/project files); otherwise omit it. Never invent, guess, or hallucinate it.",
"Name of the server the DAX was taken from. Optional; anonymous usage statistics only, does not affect formatting (the server hashes it before forwarding). Pass a real value only if you can read it from the active connection or the model/project files; otherwise omit it. Never invent or guess it.",
),
databaseName: z
.string()
.optional()
.describe(
"Name of the database/model the DAX expressions were taken from. Anonymous usage statistics only (does not affect formatting); this server SHA-256 hashes it before forwarding to daxformatter.com, so the statistics service only ever sees the hash. Pass it only if you can retrieve a real value (e.g. from the active connection or the model/project files); otherwise omit it. Never invent, guess, or hallucinate it.",
"Name of the database/model the DAX was taken from. Optional; anonymous usage statistics only, does not affect formatting (the server hashes it before forwarding). Pass a real value only if you can read it from the active connection or the model/project files; otherwise omit it. Never invent or guess it.",
),
});

Expand Down
23 changes: 3 additions & 20 deletions mcp/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
type DaxFormatServiceFactory,
FORMAT_DAX_TOOL_DESCRIPTION,
FORMAT_DAX_TOOL_NAME,
defaultServiceFactory,
formatDaxInputSchema,
Expand All @@ -9,36 +10,18 @@ import {
} from "./format-tool";
import { SERVER_NAME, SERVER_VERSION } from "./version";

const INSTRUCTIONS = `# DAX Formatter MCP

Formats DAX (Data Analysis Expressions) for Power BI, Analysis Services, and Tabular models via the SQLBI daxformatter.com service. This is the single canonical channel for DAX formatting.

Rules:
- Canonical channel: when DAX needs formatting, use this server's tools; never call daxformatter.com directly.
- Input integrity: send the user's DAX to the tool exactly as provided, byte-for-byte; do not alter, "fix", or reformat it first. Propose or apply changes only if the user explicitly asked.
- Surface errors verbatim: when the tool returns syntax errors, relay them to the user exactly as received (line, column, message); they are diagnostics the user needs.`;

const TOOL_DESCRIPTION = `Format and validate one or more DAX expressions with the DAX Formatter service (daxformatter.com). \
Use it whenever DAX needs pretty-printing or a syntax check; several expressions can be formatted in a single call. \
Returns one result per input expression, in order: the formatted text plus any syntax errors (line, column, message). \
An expression that cannot be parsed comes back with a null 'formatted' value and a non-empty 'errors' list. \
Syntax errors are normal results here, not a tool failure.`;

/**
* Builds a configured MCP server. The DAX Formatter service factory is injectable so tests can
* supply a fake; production uses {@link defaultServiceFactory}.
*/
export function createServer(factory: DaxFormatServiceFactory = defaultServiceFactory): McpServer {
const server = new McpServer(
{ name: SERVER_NAME, version: SERVER_VERSION },
{ instructions: INSTRUCTIONS },
);
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });

server.registerTool(
FORMAT_DAX_TOOL_NAME,
{
title: "Format DAX",
description: TOOL_DESCRIPTION,
description: FORMAT_DAX_TOOL_DESCRIPTION,
inputSchema: formatDaxInputSchema,
outputSchema: formatDaxOutputSchema,
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
Expand Down