@bcility/al-performance-mcp
v2.1.3
Published
MCP server for Business Central AL performance pattern analysis — detects 38 anti-patterns with SQL impact data and workshop hints
Downloads
15
Maintainers
Readme
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
| 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-diveThe 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
npxlauncher) - Python 3.11+ with
uv(recommended) orpip - 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:
{
"servers": {
"al-performance": {
"type": "stdio",
"command": "npx",
"args": ["-y", "al-performance-mcp"]
}
}
}Claude Desktop
Add to %APPDATA%\Claude\claude_desktop_config.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
git clone https://github.com/BCILITY-DOO/MCP-AL-Performance.git
cd MCP-AL-Performance
pip install -r requirements.txtThen 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(...):
@_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 reportSeverity 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





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 |
