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
59 changes: 56 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
name: CI
name: Build

on:
push:
branches: [main]
pull_request:
branches: [main]
branches: [main, dev]
release:
types: [created, published]

permissions:
contents: write

jobs:
build-and-test:
runs-on: ubuntu-latest
runs-on: windows-latest

steps:
- uses: actions/checkout@v4
Expand All @@ -34,3 +39,51 @@ jobs:

- name: Run tests
run: dotnet test tests/PlanViewer.Core.Tests/PlanViewer.Core.Tests.csproj -c Release --no-build --verbosity normal

- name: Get version
id: version
shell: pwsh
run: |
$version = ([xml](Get-Content src/PlanViewer.App/PlanViewer.App.csproj)).Project.PropertyGroup.Version | Where-Object { $_ }
echo "VERSION=$version" >> $env:GITHUB_OUTPUT

- name: Publish App
run: dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -o publish/App

- name: Publish CLI
run: dotnet publish src/PlanViewer.Cli/PlanViewer.Cli.csproj -c Release -o publish/Cli

- name: Package release artifacts
if: github.event_name == 'release'
shell: pwsh
run: |
$version = "${{ steps.version.outputs.VERSION }}"
New-Item -ItemType Directory -Force -Path releases

# App ZIP
if (Test-Path 'README.md') { Copy-Item 'README.md' 'publish/App/' }
if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' 'publish/App/' }
Compress-Archive -Path 'publish/App/*' -DestinationPath "releases/PerformanceStudio-$version.zip" -Force

# CLI ZIP
if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' 'publish/Cli/' }
Compress-Archive -Path 'publish/Cli/*' -DestinationPath "releases/PerformanceStudioCli-$version.zip" -Force

- name: Generate checksums
if: github.event_name == 'release'
shell: pwsh
run: |
$checksums = Get-ChildItem releases/*.zip | ForEach-Object {
$hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLower()
"$hash $($_.Name)"
}
$checksums | Out-File -FilePath releases/SHA256SUMS.txt -Encoding utf8
Write-Host "Checksums:"
$checksums | ForEach-Object { Write-Host $_ }

- name: Upload release assets
if: github.event_name == 'release'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload ${{ github.event.release.tag_name }} releases/*.zip releases/SHA256SUMS.txt --clobber
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributing to SQL Performance Studio
# Contributing to Performance Studio

Thank you for your interest in contributing to SQL Performance Studio! This guide will help you get started.
Thank you for your interest in contributing to Performance Studio! This guide will help you get started.

## Reporting Issues

Expand Down
57 changes: 50 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SQL Performance Studio
# Performance Studio

A cross-platform SQL Server execution plan analyzer. Parses `.sqlplan` XML, identifies performance problems, suggests missing indexes, and provides actionable warnings — from the command line or a desktop GUI.
A cross-platform SQL Server execution plan analyzer with built-in MCP server for AI-assisted analysis. Parses `.sqlplan` XML, identifies performance problems, suggests missing indexes, and provides actionable warnings — from the command line or a desktop GUI.

Built for developers and DBAs who want fast, automated plan analysis without clicking through SSMS.

Expand Down Expand Up @@ -139,7 +139,7 @@ planview analyze ./queries/ --server sql2022 --database StackOverflow2013 \
```

Batch mode produces three files per query:
- `query_name.sqlplan` — the raw execution plan XML (openable in SSMS or the SQL Performance Studio GUI)
- `query_name.sqlplan` — the raw execution plan XML (openable in SSMS or the Performance Studio GUI)
- `query_name.analysis.json` — structured analysis with warnings, missing indexes, and operator tree
- `query_name.analysis.txt` — human-readable text report

Expand Down Expand Up @@ -240,6 +240,7 @@ Features:
- **Copy Repro Script** — extracts parameters, SET options, and query text into a runnable `sp_executesql` script
- **Get Actual Plan** — connect to a server and re-execute the query to capture runtime stats
- **Query Store Analysis** — connect to a server and analyze top queries by CPU, duration, or reads
- **MCP Server** — built-in Model Context Protocol server for AI-assisted plan analysis (opt-in)
- Dark theme

```bash
Expand All @@ -248,14 +249,14 @@ dotnet run --project src/PlanViewer.App

## SSMS Extension

A VSIX extension that adds **"Open in SQL Performance Studio"** to the execution plan right-click context menu in SSMS 18-22.
A VSIX extension that adds **"Open in Performance Studio"** to the execution plan right-click context menu in SSMS 18-22.

### How it works

1. Right-click on any execution plan in SSMS
2. Click "Open in SQL Performance Studio"
2. Click "Open in Performance Studio"
3. The extension extracts the plan XML via reflection and saves it to a temp file
4. SQL Performance Studio opens with the plan loaded
4. Performance Studio opens with the plan loaded

### Installation

Expand All @@ -267,13 +268,55 @@ A VSIX extension that adds **"Open in SQL Performance Studio"** to the execution

### First run

On first use, if SQL Performance Studio isn't found automatically, the extension will prompt you to locate `PlanViewer.App.exe`. The path is saved to the registry (`HKCU\SOFTWARE\DarlingData\SQLPerformanceStudio\InstallPath`) so you only need to do this once.
On first use, if Performance Studio isn't found automatically, the extension will prompt you to locate `PlanViewer.App.exe`. The path is saved to the registry (`HKCU\SOFTWARE\DarlingData\SQLPerformanceStudio\InstallPath`) so you only need to do this once.

The extension searches for the app in this order:
1. Registry key (set automatically after first browse)
2. System PATH
3. Common install locations (`%LOCALAPPDATA%\Programs\SQLPerformanceStudio\`, `Program Files`, etc.)

## MCP Server (LLM Integration)

The desktop GUI includes an embedded [Model Context Protocol](https://modelcontextprotocol.io) server that exposes loaded execution plans and Query Store data to LLM clients like Claude Code and Cursor.

### Setup

1. Enable the MCP server in `~/.planview/settings.json`:

```json
{
"mcp_enabled": true,
"mcp_port": 5152
}
```

2. Register with Claude Code:

```
claude mcp add --transport streamable-http --scope user performance-studio http://localhost:5152/
```

3. Open a new Claude Code session and ask questions like:
- "What plans are loaded in the application?"
- "Analyze the execution plan and tell me what's wrong"
- "Are there any missing index suggestions?"
- "Compare these two plans — which is better?"
- "Fetch the top 10 queries by CPU from Query Store"

### Available Tools

13 tools for plan analysis and Query Store data:

| Category | Tools |
|---|---|
| Discovery | `list_plans`, `get_connections` |
| Plan Analysis | `analyze_plan`, `get_plan_summary`, `get_plan_warnings`, `get_missing_indexes`, `get_plan_parameters`, `get_expensive_operators`, `get_plan_xml`, `compare_plans`, `get_repro_script` |
| Query Store | `check_query_store`, `get_query_store_top` |

Plan analysis tools work on plans loaded in the app (via file open, paste, query execution, or Query Store fetch). Query Store tools use a built-in read-only DMV query — no arbitrary SQL can be executed.

The MCP server binds to `localhost` only and does not accept remote connections. Disabled by default.

## Project Structure

```
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Reporting a Vulnerability

If you discover a security vulnerability in SQL Performance Studio, please report it responsibly.
If you discover a security vulnerability in Performance Studio, please report it responsibly.

**Do not open a public GitHub issue for security vulnerabilities.**

Expand All @@ -26,7 +26,7 @@ This policy applies to:

## Security Best Practices

When using SQL Performance Studio:
When using Performance Studio:

- Use Windows Authentication where possible when connecting to SQL Server
- Use dedicated accounts with minimal required permissions
Expand Down
Empty file added screenshots/.gitkeep
Empty file.
26 changes: 21 additions & 5 deletions src/PlanViewer.App/AboutWindow.axaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PlanViewer.App.AboutWindow"
Title="About SQL Performance Studio"
Width="450" Height="340"
Title="About Performance Studio"
Width="450" Height="420"
CanResize="False"
WindowStartupLocation="CenterOwner"
Icon="avares://PlanViewer.App/EDD.ico"
Expand All @@ -11,7 +11,7 @@
<Grid Margin="24" RowDefinitions="Auto,Auto,Auto,Auto,*,Auto">

<!-- Title -->
<TextBlock Grid.Row="0" Text="SQL Performance Studio" FontWeight="Bold" FontSize="20"
<TextBlock Grid.Row="0" Text="Performance Studio" FontWeight="Bold" FontSize="20"
Foreground="{DynamicResource ForegroundBrush}" Margin="0,0,0,4"/>
<TextBlock Grid.Row="1" x:Name="VersionText" Text="Version 0.3.0" FontSize="12"
Foreground="{DynamicResource ForegroundMutedBrush}" Margin="0,0,0,4"/>
Expand Down Expand Up @@ -46,8 +46,24 @@
TextDecorations="Underline"/>
</StackPanel>

<!-- Spacer -->
<Panel Grid.Row="4"/>
<!-- MCP Settings -->
<StackPanel Grid.Row="4" Margin="0,0,0,12">
<TextBlock Text="Settings" FontWeight="SemiBold" FontSize="13"
Foreground="{DynamicResource ForegroundBrush}" Margin="0,0,0,8"/>
<StackPanel Orientation="Horizontal" Spacing="12">
<CheckBox x:Name="McpEnabledCheckBox" Content="Enable MCP Server"
FontSize="12" VerticalContentAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}"/>
<TextBlock Text="Port:" FontSize="12" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}"/>
<TextBox x:Name="McpPortInput" Width="70" Height="28"
Text="5152" FontSize="12" Padding="6,2"
VerticalContentAlignment="Center"/>
</StackPanel>
<TextBlock Text="Restart the application after changing MCP settings."
FontSize="11" Foreground="{DynamicResource ForegroundMutedBrush}"
Margin="0,4,0,0"/>
</StackPanel>

<!-- Close -->
<Button Grid.Row="5" Content="Close" Click="CloseButton_Click"
Expand Down
31 changes: 30 additions & 1 deletion src/PlanViewer.App/AboutWindow.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
/*
* SQL Performance Studio — SQL Server Execution Plan Analyzer
* Performance Studio — SQL Server Execution Plan Analyzer
* Copyright (c) 2026 Erik Darling, Darling Data LLC
* Licensed under the MIT License - see LICENSE file for details
*/

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using PlanViewer.App.Mcp;

namespace PlanViewer.App;

Expand All @@ -25,6 +29,31 @@ public AboutWindow()
var version = Assembly.GetExecutingAssembly().GetName().Version;
if (version != null)
VersionText.Text = $"Version {version.Major}.{version.Minor}.{version.Build}";

// Load current MCP settings
var settings = McpSettings.Load();
McpEnabledCheckBox.IsChecked = settings.Enabled;
McpPortInput.Text = settings.Port.ToString();

// Save on change
McpEnabledCheckBox.IsCheckedChanged += (_, _) => SaveMcpSettings();
McpPortInput.LostFocus += (_, _) => SaveMcpSettings();
}

private void SaveMcpSettings()
{
var settingsDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".planview");
var settingsFile = Path.Combine(settingsDir, "settings.json");

var json = JsonSerializer.Serialize(new
{
mcp_enabled = McpEnabledCheckBox.IsChecked == true,
mcp_port = int.TryParse(McpPortInput.Text, out var p) && p >= 1024 && p <= 65535 ? p : 5152
}, new JsonSerializerOptions { WriteIndented = true });

Directory.CreateDirectory(settingsDir);
File.WriteAllText(settingsFile, json);
}

private void GitHubLink_Click(object? sender, PointerPressedEventArgs e) => OpenUrl(GitHubUrl);
Expand Down
4 changes: 2 additions & 2 deletions src/PlanViewer.App/App.axaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PlanViewer.App.App"
Name="SQL Performance Studio"
Name="Performance Studio"
RequestedThemeVariant="Dark">

<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="About SQL Performance Studio" Click="OnAboutClicked" />
<NativeMenuItem Header="About Performance Studio" Click="OnAboutClicked" />
</NativeMenu>
</NativeMenu.Menu>

Expand Down
36 changes: 36 additions & 0 deletions src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Avalonia.Controls.Templates;
using Avalonia.Platform.Storage;
using PlanViewer.App.Helpers;
using PlanViewer.App.Mcp;
using PlanViewer.Core.Models;
using PlanViewer.Core.Services;

Expand Down Expand Up @@ -52,6 +53,7 @@ private static string FormatDuration(long ms)

public partial class PlanViewerControl : UserControl
{
private readonly string _mcpSessionId = Guid.NewGuid().ToString();
private ParsedPlan? _currentPlan;
private PlanStatement? _currentStatement;
private string? _queryText;
Expand Down Expand Up @@ -175,10 +177,36 @@ public void LoadPlan(string planXml, string label, string? queryText = null)
PopulateStatementsGrid(allStatements);
ShowStatementsPanel();
StatementsGrid.SelectedIndex = 0;

// Register with MCP session manager for AI tool access
// Count warnings from both statement-level PlanWarnings and all node Warnings
int warningCount = 0, criticalCount = 0;
foreach (var s in allStatements)
{
warningCount += s.PlanWarnings.Count;
criticalCount += s.PlanWarnings.Count(w => w.Severity == PlanWarningSeverity.Critical);
if (s.RootNode != null)
CountNodeWarnings(s.RootNode, ref warningCount, ref criticalCount);
}

PlanSessionManager.Instance.Register(_mcpSessionId, new PlanSession
{
SessionId = _mcpSessionId,
Label = label,
Source = "file",
Plan = _currentPlan,
QueryText = queryText,
StatementCount = allStatements.Count,
HasActualStats = allStatements.Any(s => s.QueryTimeStats != null),
WarningCount = warningCount,
CriticalWarningCount = criticalCount,
MissingIndexCount = _currentPlan.AllMissingIndexes.Count
});
}

public void Clear()
{
PlanSessionManager.Instance.Unregister(_mcpSessionId);
PlanCanvas.Children.Clear();
_nodeBorderMap.Clear();
_currentPlan = null;
Expand All @@ -195,6 +223,14 @@ public void Clear()
ClosePropertiesPanel();
}

private static void CountNodeWarnings(PlanNode node, ref int total, ref int critical)
{
total += node.Warnings.Count;
critical += node.Warnings.Count(w => w.Severity == PlanWarningSeverity.Critical);
foreach (var child in node.Children)
CountNodeWarnings(child, ref total, ref critical);
}

private void RenderStatement(PlanStatement statement)
{
_currentStatement = statement;
Expand Down
Loading
Loading