# AL Performance MCP Server

An MCP (Model Context Protocol) server that brings **AL performance analysis and auto-fixing** directly into your AI assistant (Claude Desktop, GitHub Copilot, Cursor, etc.).

It detects 38 performance anti-patterns across 12 categories, ranks findings by severity, generates a prioritized action plan, and auto-fixes a subset of issues — all without leaving your chat interface.

---

## Table of Contents

- [What It Does](#what-it-does)
- [Architecture](#architecture)
- [Setup](#setup)
- [Available Tools](#available-tools)
- [Pattern Reference](#pattern-reference)
- [How It Works](#how-it-works)
- [Demo](#demo)

---

## What It Does

| Capability | Details |
|---|---|
| **Detect** | 38 patterns across 12 performance categories |
| **Auto-fix** | 11 patterns can be fixed automatically (dry-run by default) |
| **Orchestrate** | One master tool delegates to 11 group sub-agents and aggregates a unified report |
| **Explain** | Per-pattern documentation with root cause, impact, and fix example |
| **Scan inline** | Paste code directly — no file path needed |
| **Scope** | Single file, full workspace, or inline snippet |

---

## Architecture

```
analyze_al_performance(folder)          ← master orchestrator
    │
    ├─ scan_data_transfer_issues()      ← sub-agent: Data Transfer
    ├─ scan_flowfield_issues()          ← sub-agent: FlowFields
    ├─ scan_aggregation_issues()        ← sub-agent: Aggregation
    ├─ scan_bulk_operation_issues()     ← sub-agent: Bulk Operations
    ├─ scan_existence_check_issues()    ← sub-agent: Existence Checks
    ├─ scan_locking_issues()            ← sub-agent: Locking
    ├─ scan_memory_issues()             ← sub-agent: Memory / Copies
    ├─ scan_short_circuit_issues()      ← sub-agent: Short-Circuit Evaluation
    ├─ scan_write_pattern_issues()      ← sub-agent: Write Patterns
    ├─ scan_cursor_safety_issues()      ← sub-agent: Cursor Safety
    └─ scan_stale_read_issues()         ← sub-agent: Stale Reads

fix_al_workspace(folder)               ← apply all auto-fixes
fix_al_file(path)                      ← apply auto-fixes to one file
scan_al_workspace(folder)              ← flat scan, full detail
scan_al_code(code)                     ← inline snippet scan
list_patterns()                        ← catalog of all 35 patterns
explain_pattern(id)                    ← per-pattern deep-dive
```

The master orchestrator runs all group sub-agents internally, aggregates every finding, scores files by weighted severity (HIGH×10 + MEDIUM×3 + LOW×1), and produces a phase-by-phase action plan.

---

## Setup

### Prerequisites

- **Node.js 18+** (for the `npx` launcher)
- **Python 3.11+** with `uv` (recommended) or `pip`
- An MCP-compatible host (Claude Desktop, GitHub Copilot agent, Cursor, etc.)

### Option A — npx (recommended, no manual install)

No clone or pip install needed. Just point your MCP host at the package:

#### VS Code (GitHub Copilot / agent mode)

Add to `.vscode/mcp.json` in your workspace:

```json
{
  "servers": {
    "al-performance": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "al-performance-mcp"]
    }
  }
}
```

#### Claude Desktop

Add to `%APPDATA%\Claude\claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "al-performance": {
      "command": "npx",
      "args": ["-y", "al-performance-mcp"]
    }
  }
}
```

#### Cursor / other MCP hosts

Use the same stdio transport pattern with `npx -y al-performance-mcp` as the command.

### Option B — local clone

```bash
git clone https://github.com/BCILITY-DOO/MCP-AL-Performance.git
cd MCP-AL-Performance
pip install -r requirements.txt
```

Then reference `server.py` directly in your MCP host config.

### Verify

In your MCP host, ask:

> "List all AL performance patterns"

You should see 38 patterns listed across 12 groups.

---

## Available Tools

### Orchestrator

| Tool | Description |
|---|---|
| `analyze_al_performance(folder_path)` | **Master orchestrator.** Runs all sub-agents, ranks files by severity, produces a phased action plan. Start here. |

### Group Sub-Agents

Each sub-agent scans one performance category with full per-file detail. Call them after `analyze_al_performance` to drill into a specific area.

| Tool | Category | Patterns |
|---|---|---|
| `scan_data_transfer_issues(folder)` | Data Transfer | MISSING_SETLOADFIELDS, FIND_DASH_BUFFER_ONE, FINDFIRST_IN_LOOP, FINDLAST_IN_LOOP, SETRANGE_FINDSET_FOR_GET, NESTED_FINDSET_N_PLUS_ONE |
| `scan_flowfield_issues(folder)` | FlowFields | CALCFIELDS_IN_LOOP, SETFILTER_ON_FLOWFIELD |
| `scan_aggregation_issues(folder)` | Aggregation | LOOP_SUM_VS_CALCSUMS, AL_SIDE_AGGREGATION |
| `scan_bulk_operation_issues(folder)` | Bulk Operations | DELETE_IN_LOOP, FINDSET_BEFORE_MODIFYALL, FINDSET_BEFORE_DELETEALL, MODIFYALL_RUNTRIGGER_TRUE, AUTOINCREMENT_DISABLES_BULK_INSERT |
| `scan_existence_check_issues(folder)` | Existence Checks | COUNT_NOT_ZERO, ISEMPTY_BEFORE_FINDSET, COUNT_EQUALS_ONE |
| `scan_locking_issues(folder)` | Locking | FINDSET_MODIFY_NO_TRUE, LOCKTABLE_FOR_SEQUENCE, LOCKTABLE_TOO_EARLY, MISSING_READ_ISOLATION, LOCKTABLE_USE_UPDLOCK |
| `scan_memory_issues(folder)` | Memory / Copies | RECORD_BY_VALUE, STRING_CONCAT_IN_LOOP, MISSING_TEMPORARY, TEMP_TABLE_AS_DICT, TEMP_TABLE_AS_COLLECTION, RECORDREF_WHEN_TYPED_SUFFICIENT |
| `scan_short_circuit_issues(folder)` | Short-Circuit Evaluation | EAGER_EVALUATION_OR_AND, OR_CHAIN_USE_CASE_TRUE, CONDITION_ORDER_TRUE_IN |
| `scan_write_pattern_issues(folder)` | Write Patterns | INSERT_ON_CONFLICT, SILENT_INSERT_FAILURE |
| `scan_cursor_safety_issues(folder)` | Cursor Safety | FILTER_MUTATION_IN_LOOP |
| `scan_stale_read_issues(folder)` | Stale Reads | MISSING_SELECTLATESTVERSION |

### Fix Tools

| Tool | Description |
|---|---|
| `fix_al_workspace(folder, dry_run=True)` | Apply all auto-fixes to every .al file. Default is dry-run (preview only). |
| `fix_al_file(file_path, dry_run=True)` | Apply all auto-fixes to a single file. |

### Utility Tools

| Tool | Description |
|---|---|
| `scan_al_workspace(folder, severity_filter, group_filter)` | Flat scan with optional severity/group filter. |
| `scan_al_code(al_code, file_hint)` | Scan a pasted AL code snippet inline. |
| `list_patterns()` | List all 38 registered patterns with ID, group, severity. |
| `explain_pattern(pattern_id)` | Full explanation of one pattern: root cause, impact, before/after fix example. |

---

## Pattern Reference

### 🔴 HIGH Severity

| Pattern ID | Group | Description |
|---|---|---|
| `MISSING_SETLOADFIELDS` | Data Transfer | Record fields loaded without SetLoadFields — fetches all columns unnecessarily |
| `FIND_DASH_BUFFER_ONE` | Data Transfer | `Find('-')` with buffer size 1 — use `FindSet()` instead |
| `FINDFIRST_IN_LOOP` | Data Transfer | `FindFirst()` inside a loop — causes N+1 queries |
| `FINDLAST_IN_LOOP` | Data Transfer | `FindLast()` inside a loop — causes N+1 queries |
| `CALCFIELDS_IN_LOOP` | FlowFields | `CalcFields()` called inside a loop — triggers one SQL aggregate per row |
| `SETFILTER_ON_FLOWFIELD` | FlowFields | `SetFilter` on a FlowField forces a correlated subquery per row |
| `LOOP_SUM_VS_CALCSUMS` | Aggregation | Manual summation loop that should use `CalcSums()` |
| `STRING_CONCAT_IN_LOOP` | Memory / Copies | String concatenation in a loop creates O(n²) allocations |
| `MISSING_TEMPORARY` | Memory / Copies | Temp table parameter or variable missing the `temporary` keyword |
| `DELETE_IN_LOOP` | Bulk Operations | `Delete()` inside a loop — use `DeleteAll()` |
| `MODIFYALL_RUNTRIGGER_TRUE` | Bulk Operations | `ModifyAll(..., true)` fires row-by-row triggers — use `false` |
| `FILTER_MUTATION_IN_LOOP` | Cursor Safety | SetRange/SetFilter mutates active FindSet cursor — corrupts iteration |
| `LOCKTABLE_FOR_SEQUENCE` | Locking | `LockTable()` used to generate a sequence — use `NumberSeriesManagement` |
| `LOCKTABLE_TOO_EARLY` | Locking | `LockTable()` called before the read/write that needs it — widens lock scope |
| `NESTED_FINDSET_N_PLUS_ONE` | Data Transfer | Nested `FindSet` inside an outer `FindSet` loop — classic N+1 query problem |
| `AUTOINCREMENT_DISABLES_BULK_INSERT` | Bulk Operations | `AutoIncrement = true` on a table field prevents SQL bulk-insert optimizations |
| `AL_SIDE_AGGREGATION` | Aggregation | `FindSet` loop accumulating values into a Dictionary — push aggregation to SQL with `CalcSums` |

### 🟡 MEDIUM Severity

| Pattern ID | Group | Description |
|---|---|---|
| `SETRANGE_FINDSET_FOR_GET` | Data Transfer | SetRange + FindFirst to fetch by PK — use `Get()` instead |
| `RECORD_BY_VALUE` | Memory / Copies | Record passed by value creates a full in-memory copy |
| `FINDSET_MODIFY_NO_TRUE` | Locking | `FindSet()` without `true` when `Modify` follows — missing row lock |
| `FINDSET_BEFORE_MODIFYALL` | Bulk Operations | Unnecessary `FindSet` before `ModifyAll` — ModifyAll doesn't need a cursor |
| `FINDSET_BEFORE_DELETEALL` | Bulk Operations | Unnecessary `FindSet` before `DeleteAll` |
| `COUNT_NOT_ZERO` | Existence Checks | `Count() <> 0` scans all rows — use `not IsEmpty()` |
| `COUNT_EQUALS_ONE` | Existence Checks | `Count() = 1` scans all rows to check uniqueness |
| `ISEMPTY_BEFORE_FINDSET` | Existence Checks | Redundant `IsEmpty` check before `FindSet` — FindSet already returns false |
| `TEMP_TABLE_AS_DICT` | Memory / Copies | Temp table used as a key-value dictionary — consider `Dictionary` type |
| `TEMP_TABLE_AS_COLLECTION` | Memory / Copies | Temp table used as a simple list — consider `List` type |
| `INSERT_ON_CONFLICT` | Write Patterns | Try-Insert then Modify on failure — use `InsertOrModify` |
| `SILENT_INSERT_FAILURE` | Write Patterns | `Insert()` without return value check — errors silently swallowed |
| `MISSING_SETCURRENTKEY` | Sort / Keys | Sorting on a non-key field without `SetCurrentKey` — triggers sort operator |
| `EAGER_EVALUATION_OR_AND` | Short-Circuit Evaluation | Expensive function on left of `or`/`and` — reorder for short-circuit |
| `OR_CHAIN_USE_CASE_TRUE` | Short-Circuit Evaluation | 3+ OR conditions — use `case true of` for short-circuit evaluation |
| `MISSING_READ_ISOLATION` | Locking | `FindSet` on a read-only query missing `ReadIsolation` hint |
| `LOCKTABLE_USE_UPDLOCK` | Locking | `LockTable()` + `FindSet()` — prefer `ReadIsolation := UpdLock` |
| `MISSING_SELECTLATESTVERSION` | Stale Reads | Multi-pass loop missing `SelectLatestVersion()` — may read stale data |

### 🔵 LOW Severity

| Pattern ID | Group | Description |
|---|---|---|
| `DELETEALL_NO_ISEMPTY_GUARD` | Locking | `DeleteAll()` without `IsEmpty` guard — acquires a lock even on empty table |
| `RECORDREF_WHEN_TYPED_SUFFICIENT` | Memory / Copies | `RecordRef` used where a typed `Record` would be more efficient |
| `CONDITION_ORDER_TRUE_IN` | Short-Circuit Evaluation | `if true in [...]` conditions not ordered cheapest-first |

### Auto-Fixable Patterns

These 11 patterns are transformed automatically by `fix_al_file` / `fix_al_workspace`:

| Pattern ID | Fix Applied |
|---|---|
| `FIND_DASH_BUFFER_ONE` | `Find('-')` → `FindSet()` |
| `COUNT_NOT_ZERO` | `Count() <> 0` → `not IsEmpty()` |
| `COUNT_EQUALS_ONE` | `Count() = 1` → `FindFirst() and (Next() = 0)` |
| `FINDSET_MODIFY_NO_TRUE` | `FindSet()` → `FindSet(true)` where Modify follows |
| `FINDSET_BEFORE_MODIFYALL` | Removes the redundant FindSet guard |
| `FINDSET_BEFORE_DELETEALL` | Removes the redundant FindSet guard |
| `FINDFIRST_IN_LOOP` | `FindFirst()` → `FindSet()` where Next() follows |
| `MISSING_TEMPORARY` | Adds `temporary` keyword to temp table parameter |
| `MODIFYALL_RUNTRIGGER_TRUE` | `ModifyAll(..., true)` → `ModifyAll(..., false)` |
| `LOCKTABLE_USE_UPDLOCK` | `LockTable()` + `FindSet()` → `ReadIsolation := UpdLock` + `FindSet(true)` |
| `ISEMPTY_BEFORE_FINDSET` | Removes redundant `if not IsEmpty() then FindSet` guard |

---

## How It Works

### Pattern Registration

Each pattern is a Python class decorated with `@_register(...)`:

```python
@_register(
    id="MISSING_SETLOADFIELDS",
    group="Data Transfer",
    severity="HIGH",
    title="Missing SetLoadFields",
    description="...",
    exercises=[1, 2, 33],
)
class PatternMissingSetLoadFields:
    @staticmethod
    def detect(text: str) -> list[dict]:
        # regex-based detection, returns list of {line, message, snippet}
        ...

    @staticmethod
    def fix(text: str) -> str:
        # text transformation, returns modified AL source
        ...
```

The decorator appends `{"id", "group", "severity", "title", "description", "detector", "fixer"}` to the global `PATTERNS` list.

### Detection

- All detection is **regex-based**, operating on raw AL source text
- Each `detect()` returns a list of findings: `{line: int, message: str, snippet: str}`
- Line numbers are computed from character offsets via `_find_line(text, offset)`
- Files are read with `encoding='utf-8-sig'` (BOM-aware, required for AL files)

### Orchestration Flow

```
analyze_al_performance(folder)
    for each group in PATTERNS:
        _run_group_agent(group, folder)
            read all .al files
            run all detectors for that group
            return {group, findings[], high, medium, low}
    aggregate all findings
    rank files by weighted severity score
    build action plan (Phase 1: auto-fix → Phase 2: HIGH manual → Phase 3: MEDIUM)
    return markdown report
```

### Severity Scoring

Files are ranked by: `score = HIGH × 10 + MEDIUM × 3 + LOW × 1`

This ensures that a file with 1 HIGH issue ranks above one with 10 LOW issues.

---

## Demo

![Scan workspace in Copilot Chat](https://cdn.jsdelivr.net/npm/@bcility/al-performance-mcp/images/demo-01.png)

![Analysis results overview](https://cdn.jsdelivr.net/npm/@bcility/al-performance-mcp/images/demo-02.png)

![Pattern details and SQL impact](https://cdn.jsdelivr.net/npm/@bcility/al-performance-mcp/images/demo-03.png)

![Fix suggestions per file](https://cdn.jsdelivr.net/npm/@bcility/al-performance-mcp/images/demo-04.png)

![Auto-fix applied](https://cdn.jsdelivr.net/npm/@bcility/al-performance-mcp/images/demo-05.png)

---

## Files

| File | Purpose |
|---|---|
| `server.py` | MCP server — all 38 patterns + 17 tools |
| `requirements.txt` | Python dependency: `mcp[cli]>=1.0.0` |
| `mcp.json` | VS Code MCP host configuration |
| `README.md` | This file |
