npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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-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:

{
  "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.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() <> 0not IsEmpty() | | COUNT_EQUALS_ONE | Count() = 1FindFirst() 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 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

Analysis results overview

Pattern details and SQL impact

Fix suggestions per file

Auto-fix applied


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 |