Production-grade Business Central AL code review engine powered by multi-model LLM consensus and 37 static analysis rules based on Microsoft CodeCop, AppSourceCop, and PerTenantExtensionCop analyzers.
All rules and standards reference the official Microsoft documentation: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/
- Overview
- Architecture
- Quick Start
- Configuration
- MCP Tools Reference
- Static Analysis Rules
- Review Modes
- Microsoft BC Development Standards
- Security Features
- Observability & Logging
- SARIF Output for CI/CD
- Module Reference
- Environment Variables
The BC Code Reviewer is a Model Context Protocol (MCP) server that reviews Business Central AL source code using two complementary analysis layers:
| Layer | Speed | Token Cost | Accuracy |
|---|---|---|---|
| Static rules (regex-based) | <100ms | Zero | High for known patterns |
| LLM consensus (multi-model) | 10-60s | Per-model tokens | High with voting |
The server runs over stdio transport and integrates with VS Code Copilot Chat, Claude Desktop, or any MCP-compatible client.
- 37 static rules aligned with official Microsoft AL analyzers (CodeCop AA, AppSourceCop AS, PTECop PTE)
- 4 LLM providers — OpenAI, Azure OpenAI, Anthropic, Ollama (local)
- 8-category LLM system prompt covering Security, Performance, Best Practices, Localizability, Upgrade, Error Handling, Events, and PTE compliance
- Multi-model consensus — findings are only reported when multiple models agree
- Per-severity thresholds — critical findings need fewer votes, info findings need more
- SARIF v2.1.0 export — for CI/CD pipeline integration (GitHub Code Scanning, Azure DevOps)
- Path traversal protection — workspace root restriction prevents file access attacks
- API key sanitization — secrets are never leaked in error messages or logs
- Rate limiting — sliding-window limiter prevents runaway LLM costs
- Retry with backoff — automatic retries on 429/502/503/504 errors
- Structured JSON logging — observability-ready log output
- Disabled rules — suppress specific rule IDs via environment variable
┌──────────────────────────────────────────────────────────────┐
│ MCP Client (VS Code / Claude) │
│ stdio transport │
└───────────────────────────┬──────────────────────────────────┘
│
┌───────────────────────────▼──────────────────────────────────┐
│ server.py (FastMCP) │
│ 7 MCP tools: review_al_code, review_al_file, │
│ review_al_folder, review_static_only, │
│ review_al_code_with_model, list_review_rules, │
│ list_configured_models │
│ │
│ Features: input validation, path traversal protection, │
│ SARIF output, min_severity filter, structured logging │
└──────────┬────────────────────────────────────┬──────────────┘
│ │
┌──────────▼──────────┐ ┌─────────────▼─────────────┐
│ reviewer.py │ │ rules.py │
│ │ │ │
│ CodeReviewAgent │◄──────────┤ 37 static rule functions │
│ - review_code() │ │ _preprocess() strips │
│ - review_file() │ │ block comments │
│ - _apply_consensus()│ │ _strip_string_literals() │
│ - _compute_score() │ │ run_static_checks() │
│ - rate limiter │ │ disabled_rules filtering │
│ - SARIF export │ └───────────────────────────┘
│ - _finding_fingerprint()
└──────────┬──────────┘
│
┌──────────▼──────────────────────────────────────────────────┐
│ models.py │
│ │
│ MultiModelClient — shared httpx.AsyncClient │
│ ├── _call_openai() (OpenAI / OpenAI-compat) │
│ ├── _call_azure_openai() (Azure OpenAI) │
│ ├── _call_anthropic() (Claude) │
│ └── _call_ollama() (Local Ollama) │
│ │
│ REVIEW_SYSTEM_PROMPT (~160 lines, 8 categories) │
│ Features: connection pooling, retry w/ exponential backoff, │
│ secret sanitization │
└──────────────────────────────────────────────────────────────┘
│
┌──────────▼──────────┐
│ config.py │
│ │
│ ReviewerConfig │
│ ModelConfig │
│ from_env() loads │
│ all settings from │
│ .env / environment │
└─────────────────────┘
# Clone the repository
git clone https://github.com/VatsaShivam/bc_code_reviewer.git
cd bc_code_reviewer
# Install in development mode
pip install -e .Or install from PyPI (once published):
pip install bc-code-reviewerCreate a .env file in the project root:
# At least one LLM provider is required for non-static reviews
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o
# Optional: Azure OpenAI
# AZURE_OPENAI_API_KEY=...
# AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
# AZURE_OPENAI_DEPLOYMENT=gpt-4o
# Optional: Anthropic
# ANTHROPIC_API_KEY=sk-ant-...
# ANTHROPIC_MODEL=claude-sonnet-4-20250514
# Optional: Ollama (local, no API key needed)
# OLLAMA_MODEL=codellama:13b
# OLLAMA_BASE_URL=http://localhost:11434bc-code-reviewerThe server can be configured in .vscode/mcp.json:
{
"servers": {
"bcCodeReviewer": {
"type": "stdio",
"command": "bc-code-reviewer",
"envFile": "${workspaceFolder}/.env"
}
}
}Once configured, invoke review tools directly from VS Code Copilot Chat.
Build and run with Docker:
# Build the image
docker build -t bc-code-reviewer .
# Run the container
docker run --env-file .env -it bc-code-reviewerAll configuration is via environment variables (loaded from .env).
| Field | Type | Default | Env Var | Description |
|---|---|---|---|---|
consensus_threshold |
float | 0.6 | REVIEWER_CONSENSUS_THRESHOLD |
Fraction of models that must agree (warnings) |
consensus_threshold_critical |
float | 0.4 | REVIEWER_CONSENSUS_THRESHOLD_CRITICAL |
Lower threshold for critical findings |
consensus_threshold_info |
float | 0.8 | REVIEWER_CONSENSUS_THRESHOLD_INFO |
Higher threshold for info findings |
max_file_size_kb |
int | 500 | REVIEWER_MAX_FILE_SIZE_KB |
Max file size for review |
max_input_bytes |
int | 512000 | REVIEWER_MAX_INPUT_BYTES |
Hard limit on input code size |
review_timeout |
float | 120.0 | REVIEWER_TIMEOUT |
LLM request timeout (seconds) |
disabled_rules |
frozenset | empty | REVIEWER_DISABLED_RULES |
Comma-separated rule IDs to suppress |
workspace_root |
str | None | REVIEWER_WORKSPACE_ROOT |
Restrict file access to this directory |
rate_limit_per_minute |
int | 30 | REVIEWER_RATE_LIMIT |
Max reviews per minute |
retry_max_attempts |
int | 3 | REVIEWER_RETRY_ATTEMPTS |
Max LLM retry attempts |
retry_backoff_base |
float | 1.0 | REVIEWER_RETRY_BACKOFF |
Base seconds for exponential backoff |
Review AL source code using multi-model consensus.
| Parameter | Type | Default | Description |
|---|---|---|---|
code |
str | required | AL source code to review |
filename |
str | "untitled.al" |
File name for context |
mode |
str | "consensus" |
"static", "single", or "consensus" |
min_severity |
str | None | Filter: "critical", "warning", or "info" |
output_format |
str | "json" |
"json" or "sarif" |
Review an AL file from disk.
| Parameter | Type | Default | Description |
|---|---|---|---|
file_path |
str | required | Path to .al file |
mode |
str | "consensus" |
Review mode |
min_severity |
str | None | Severity filter |
output_format |
str | "json" |
Output format |
Review all .al files in a directory recursively.
| Parameter | Type | Default | Description |
|---|---|---|---|
folder_path |
str | required | Path to folder |
mode |
str | "static" |
Review mode |
max_files |
int | 50 | Safety limit (max 200) |
min_severity |
str | None | Severity filter |
Fast static-analysis-only check (zero LLM cost).
| Parameter | Type | Default | Description |
|---|---|---|---|
code |
str | required | AL source code |
min_severity |
str | None | Severity filter |
Review using a specific LLM provider (bypass consensus).
| Parameter | Type | Default | Description |
|---|---|---|---|
code |
str | required | AL source code |
provider |
str | required | "openai", "azure_openai", "anthropic", "ollama" |
filename |
str | "untitled.al" |
File name for context |
min_severity |
str | None | Severity filter |
List all 37 static rules with IDs, descriptions, and disabled status. Auto-generated from the rule engine.
List all active LLM providers and consensus thresholds.
37 rules aligned with official Microsoft analyzers. All run locally at zero token cost.
Reference: CodeCop Analyzer Rules | AppSourceCop Rules | PerTenantExtensionCop Rules
| Rule ID | Severity | Description | MS Doc |
|---|---|---|---|
| AA0005 | info | Only use BEGIN..END to enclose compound statements |
AA0005 |
| AA0008 | warning | Function calls should have parentheses even with no parameters | AA0008 |
| AA0013 | warning | When BEGIN follows THEN/ELSE/DO, it should be on the same line |
AA0013 |
| AA0074 | warning | TextConst/Label variable names should have approved suffix (Lbl, Tok, Msg, Err, Qst) | AA0074 |
| AA0100 | info | Do not have identifiers with quotes in the name | AA0100 |
| Rule ID | Severity | Description | MS Doc |
|---|---|---|---|
| AA0003 | warning | Explicit COMMIT() breaking transactional integrity |
AA0003 |
| AA0040 | warning | Avoid nested WITH statements |
AA0040 |
| AA0073 | warning | Temporary variable must be prefixed with Temp |
AA0073 |
| AA0137 | warning | Unused variable declarations | AA0137 |
| AA0161 | warning | Only use AssertError in Test Codeunits |
AA0161 |
| AA0189 | warning | Missing ApplicationArea on page field/action |
AA0189 |
| AA0207 | warning | EventSubscriber method must be local |
AA0207 |
| AA0213 | warning | Obsolete/deprecated AL patterns | AA0213 |
| AA0237 | warning | Non-temporary variable with Temp prefix |
AA0237 |
| AA0247 | info | Object missing namespace declaration |
AA0247 |
| AA0101 | warning | Use camelCase property values in pages of type API | AA0101 |
| Rule ID | Severity | Description | MS Doc |
|---|---|---|---|
| AA0181 | warning | FindSet/Find must be used with Next() |
AA0181 |
| AA0211 | warning | CalcFields should only be on FlowField or Blob fields |
AA0211 |
| AA0222 | warning | SIFT index should not be on primary or unique key | AA0222 |
| AA0233 | warning | Get/FindFirst/FindLast should not be used with Next() |
AA0233 |
| AA0242 | warning | FindSet/FindFirst without SetLoadFields — use partial records |
AA0242 |
| Rule ID | Severity | Description | MS Doc |
|---|---|---|---|
| AA0175 | info | Get() called with no parameters |
AA0175 |
| AA0214 | warning | Insert/Modify/Delete without return check |
AA0214 |
| AA0231 | warning | StrSubstNo/concatenation in Error() call |
AA0231 |
| Rule ID | Severity | Description | MS Doc |
|---|---|---|---|
| AA0216 | warning | Hardcoded strings in Message/Error/Confirm |
AA0216 |
| AA0217 | warning | StrSubstNo with inline string instead of Label |
AA0217 |
| AA0218 | warning | Page field/action missing ToolTip property |
AA0218 |
| AA0219 | info | ToolTip should start with "Specifies" |
AA0219 |
| AA0225 | warning | Page field missing Caption property |
AA0225 |
| AA0234 | info | Table field missing ToolTip property |
AA0234 |
| AA0448 | warning | Use FieldCaption/TableCaption instead of FieldName/TableName |
AA0448 |
| AA0462 | warning | CalcDate should use DateFormula variables or <> syntax |
AA0462 |
| AA0470 | warning | Label placeholders should have explaining Comment | AA0470 |
| Rule ID | Severity | Description | MS Doc |
|---|---|---|---|
| AA0087 | warning | InherentPermissions (lowering permissions) should only be in tests |
AA0087 |
| AA0240 | warning | Email/phone in source code | AA0240 |
| AS0016 | critical | Table field missing DataClassification (table + tableextension) |
AS0016 |
| PTE0006 | critical | Encryption key functions must not be invoked | PTE0006 |
| Rule ID | Severity | Description | MS Doc |
|---|---|---|---|
| PTE0003 | critical | Subscribing to CompanyOpen events is not allowed |
PTE0003 |
Before rules execute, the engine:
- Strips
/* */block comments — preserves line numbers for accurate reporting - Strips string literals (for email/phone rule) — avoids false positives on URLs in Labels
- Preserves raw lines for code snippets in findings
# Suppress specific rules (comma-separated, case-insensitive)
REVIEWER_DISABLED_RULES=AA0219,AA0100,AA0247- Speed: <100ms
- Cost: Zero tokens
- Use case: Fast CI gate, pre-commit hook
- Runs all 37 regex-based rules against preprocessed code
- Speed: 5-30s
- Cost: One model's tokens
- Use case: Quick feedback from preferred model
- Static rules + single LLM provider analysis
- Speed: 10-60s (parallel LLM calls)
- Cost: All configured models' tokens
- Use case: High-confidence production reviews
- LLM findings are deduplicated and voted on:
| Finding Severity | Consensus Threshold | Min Votes (3 models) |
|---|---|---|
| critical | 0.4 (40%) | 2 |
| warning | 0.6 (60%) | 2 |
| info | 0.8 (80%) | 3 |
The following standards are enforced through both static rules and the LLM system prompt, based entirely on the official Microsoft documentation.
Source: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/
Per AL Coding Guidelines:
| Rule | Standard | Example |
|---|---|---|
| AA0072 | Variable/parameter names suffixed with type/object name | CustomerRec: Record Customer; |
| AA0073 | Temporary variables prefixed with Temp |
TempSalesLine: Record "Sales Line" temporary; |
| AA0237 | Non-temporary variables must NOT have Temp prefix |
SalesLine: Record "Sales Line"; |
| AA0074 | Label/TextConst variables have approved suffixes | ConfirmQst, ErrorErr, DescriptionLbl, SeparatorTok |
| AA0215 | PascalCase for objects, methods, properties | procedure CalculateTotal() |
| AA0100 | No quoted identifiers unless spaces/reserved words required | CustomerName not "CustomerName" |
| AA0101-104 | camelCase for API page/query entity names and columns | entityName = 'salesOrder' |
| AS0011 | Mandatory affixes for AppSource extensions | FSO_ prefix or _FSO suffix |
| AA0247 | Use namespaces with at least two levels | namespace MyCompany.MyApp; |
| AS0127 | Namespaces should have at least two levels | namespace Company.Module; |
Per Performance Articles for Developers:
| Standard | Description | Rule |
|---|---|---|
| Partial Records | Always use SetLoadFields() before FindSet/FindFirst to avoid JIT loading of table extension fields |
AA0242 |
| FindSet vs FindFirst | Use FindFirst() when only one record needed. FindSet() requires Next() for looping |
AA0181, AA0233 |
| Set-based operations | Prefer CalcFields, CalcSums, SetAutoCalcFields over manual loops |
LLM |
| Built-in data structures | Use TextBuilder (5+ concatenations), Dictionary (key-value lookups), List (dynamic arrays) |
LLM |
| Background tasks | Use Page Background Task for calculated values in cues/FactBoxes |
LLM |
| Defer LockTable | Call Record.LockTable as late as possible to minimize lock duration |
LLM |
| Tri-state locking | Enable tri-state locking to avoid reads-after-writes taking locks | LLM |
| Read Scale-Out | Set DataAccessIntent = ReadOnly on reports, API pages, and queries |
LLM |
| Indexing | Design proper keys/indexes. Avoid filtering on non-indexed fields (AA0210) | LLM |
| SIFT | Use SumIndexField Technology for FlowFields. Don't put SIFT on primary key (AA0222) | AA0222 |
| OnAfterGetRecord | Avoid CalcFields, CurrPage.Update(), filter changes, and DB writes in this trigger |
LLM |
| Media vs Blob | Use Media/MediaSet data types instead of Blob for images (client caching + thumbnails) |
LLM |
Per CodeCop Localizability Rules:
| Standard | Description | Rules |
|---|---|---|
| No hardcoded strings | All user-facing strings must use Label or TextConst variables |
AA0216, AA0217 |
| ToolTip required | Every page field/action must have a non-empty ToolTip property | AA0218, AA0220 |
| ToolTip format | Field ToolTips should start with "Specifies" | AA0219 |
| Table field ToolTip | Table fields should also have ToolTip properties | AA0234 |
| Caption required | Page fields must have a non-empty Caption property | AA0225, AA0226 |
| OptionCaption | Required for non-table-field source expressions | AA0221, AA0223, AA0224 |
| FieldCaption over FieldName | Use FieldCaption() and TableCaption() for localization |
AA0448 |
| Label placeholders | Placeholders in Labels must have explanatory Comment property |
AA0470 |
| Placeholder matching | String parameters must match the number of placeholders | AA0131 |
| CalcDate format | Use DateFormula variables or <> enclosed strings with CalcDate |
AA0462 |
| TranslationFile | TranslationFile must be enabled (AS0015) |
AS0015 |
Per AppSourceCop Security Rules:
| Standard | Description | Rules |
|---|---|---|
| DataClassification | Every table field of class 'Normal' must have DataClassification set. ToBeClassified is NOT acceptable for submission |
AS0016 |
| Valid classifications | CustomerContent, EndUserIdentifiableInformation, AccountData, OrganizationIdentifiableInformation, SystemMetadata |
AS0016 |
| No PII in code | Email addresses and phone numbers must not appear in source code | AA0240 |
| Encryption keys | Encryption key functions (CreateEncryptionKey, DeleteEncryptionKey, etc.) must not be invoked |
PTE0006 |
| Permissions | InherentPermissions (lowering permissions) should only be used in test codeunits |
AA0087 |
| InternalsVisibleTo | Must not be used as a security feature | AS0081, PTE0012 |
| Permission sets | Table definitions must have a matching permission set | PTE0004, AS0103 |
| Permission set XML | Permission Sets should not be defined in XML files | AS0094, PTE0014 |
| Unsafe methods | Unsafe methods cannot be invoked in AppSource applications | AS0060 |
| Reserved tables | Reserved database tables are read-only in multi-tenant environments | AS0059 |
| Standard | Description | Rules |
|---|---|---|
| Check return values | Use Insert(true) to raise error on failure, or check with if Rec.Insert() then |
AA0214 |
| Error with Label | Never use string concatenation or StrSubstNo directly in Error() calls |
AA0231 |
| TESTFIELD | Validate key fields before record operations | AA0175 |
| COMMIT | Avoid explicit COMMIT() — let the platform manage transactions |
AA0003 |
| AssertError | Only use in Test Codeunits | AA0161, AS0058, PTE0007 |
Per AppSourceCop Upgrade Rules:
| Standard | Description | Rules |
|---|---|---|
| Don't delete published tables | Tables and table extensions that have been published must not be deleted | AS0001 |
| Don't delete published fields | Fields must not be deleted from published extensions | AS0002 |
| Don't change field types | Published fields must not change type | AS0004 |
| Don't rename tables | Published tables must not change name | AS0006 |
| Don't change keys | Key fields must not be changed or deleted | AS0009, AS0010 |
| Obsolete lifecycle | Use Pending -> Removed transition with justification and ObsoleteTag |
AA0213, AS0073, AS0075, AS0115 |
| Public API stability | Published procedures, events, and return types cannot be removed/changed | AS0018-AS0028 |
| Extension name stability | Extension name and publisher cannot be changed | AS0096, AS0097 |
| Standard | Description | Rules |
|---|---|---|
| Local subscribers | EventSubscriber methods must be local |
AA0207 |
| No CompanyOpen | Must not subscribe to CompanyOpen events (blocks session creation) |
PTE0003, AS0061 |
| Affix on procedures | Procedures in extension objects require mandatory affixes | AS0079 |
| Integration events | Use Business/Integration events for extensibility, avoid tight coupling | LLM |
| Subscriber codeunit size | Keep subscriber codeunits small. Use single-instance when possible | LLM |
| External business events | Should have obsolete marker before removal | AA0250, AA0251, AS0135 |
| Limit OnCompanyOpen work | Minimize logic in sign-in event subscribers to avoid blocking sessions | LLM |
Per PerTenantExtensionCop Rules:
| Standard | Description | Rules |
|---|---|---|
| Object ID range | Object IDs must be in the free range (50000-99999 for PTE) | PTE0001 |
| Field ID range | Field IDs must be in the free range | PTE0002 |
| ApplicationArea | All page controls and actions must set ApplicationArea |
PTE0008 |
| No encryption keys | Encryption key functions are forbidden | PTE0006 |
| No test assertions | Test assertion functions not allowed in non-test context | PTE0007 |
| Permission sets | Table definitions must have matching permission sets | PTE0004 |
| Compilation target | Must be allowed in multi-tenant SaaS environment | PTE0005 |
| No entitlements | Entitlements cannot be defined in PTE extensions | PTE0013 |
Per Writing Efficient Web Services:
| Standard | Description |
|---|---|
| API pages over SOAP | Use API pages/queries instead of SOAP endpoints (up to 10x faster) |
| No UI pages as endpoints | Avoid exposing standard UI pages as web service endpoints |
| Conditional triggers | Make AL code conditional on ClientType when page is exposed as endpoint |
| OData $batch | Use $batch for sequential child record inserts to avoid parent record locks |
| Throttling handling | Handle HTTP 429 (Too Many Requests), 503 (Service Unavailable), 504 (Gateway Timeout) |
| Retry strategies | Implement exponential backoff with randomization for throttled requests |
| API versioning | Use the highest API version available. Don't use beta API pages |
| ReadOnly intent | Set DataAccessIntent = ReadOnly on API queries consumed by Power BI |
| Avoid temp tables | Don't use temp tables as API source if >100 records (no caching, no efficient paging) |
| Standard | Description |
|---|---|
| Minimize parts | Reduce the number of page parts on role centers |
| Remove unneeded CalcFields | Remove calculated fields from lists if not needed (setting Visible=false is NOT enough) |
| Dedicated lookups | Create dedicated lookup pages instead of using full list pages for dropdowns |
| Page Background Tasks | Use for calculated values in cues to keep UI responsive |
| Edit-in-Excel | Make AL code conditional on ClientType — avoid FactBox updates and defaulting logic |
| Rendering property | Use DefaultRenderingLayout with rendering syntax instead of deprecated ExcelLayout/RDLCLayout/WordLayout |
| Standard | Description |
|---|---|
| Proper indexing | Design appropriate keys for the way code accesses data. Avoid too many indexes |
| SystemModifiedAt | Use SystemModifiedAt field for delta reads / change tracking |
| NCCI | Use Non-clustered Columnstore Indexes for real-time analytics (runtime 19.0+) |
| LockTable defer | Call Record.LockTable as late as possible in AL code |
| Record isolation | Set record instance isolation level for concurrent transaction safety |
| Bulk inserts | Leverage bulk insert mode; avoid test framework during insert/update performance tests |
| Number sequences | Use NumberSequence data type for fast, non-blocking number sequences (accepts holes) |
When REVIEWER_WORKSPACE_ROOT is set, all file access is restricted:
REVIEWER_WORKSPACE_ROOT=c:\MyProjectreview_al_file("c:\MyProject\src\MyPage.al")-> Allowedreview_al_file("c:\etc\passwd")-> Access denied- Symlinks are rejected
Error messages are sanitized before being returned to clients or logged. Patterns caught: api_key, authorization, bearer, x-api-key, password, secret, token, URL query parameters.
- Max input size enforced (
max_input_bytes, default 512KB) - Null bytes stripped from input
- Empty/whitespace-only input rejected
- File extensions validated (
.alonly) - File size checked before reading (
max_file_size_kb)
Sliding-window rate limiter prevents runaway costs:
REVIEWER_RATE_LIMIT=30 # max reviews per minuteStandard Python logging to stderr with structured JSON events:
INFO:bc_code_reviewer.reviewer:{"event": "review_start", "review_id": "review-a1b2c3d4e5f6", ...}
INFO:bc_code_reviewer.reviewer:{"event": "review_complete", "score": 7.5, "duration_ms": 234.5}
Enable JSON-formatted logs for log aggregation (ELK, Datadog, etc.):
REVIEWER_LOG_FORMAT=json| Event | Fields | Description |
|---|---|---|
server_start |
models, disabled_rules, workspace_root | Server initialization |
review_start |
review_id, filename, mode, code_size | Review initiated |
static_complete |
review_id, finding_count | Static analysis done |
llm_complete |
review_id, models, total_findings | LLM analysis done |
review_complete |
review_id, filename, score, finding_count, duration_ms | Review finished |
Export findings in SARIF v2.1.0 format for integration with GitHub Code Scanning, Azure DevOps, or any SARIF-compatible tool.
review_al_code(code="...", output_format="sarif")
review_al_file(file_path="MyPage.al", output_format="sarif")
bc_code_reviewer/
├── __init__.py # Package entry point, exports main()
├── config.py # ReviewerConfig + ModelConfig dataclasses
├── models.py # Multi-provider LLM client (OpenAI/Azure/Anthropic/Ollama)
├── reviewer.py # CodeReviewAgent — orchestrates static + LLM analysis
├── rules.py # 37 static regex-based rule functions
└── server.py # FastMCP server exposing 7 tools
| Module | Key Classes/Functions |
|---|---|
config.py |
ModelConfig, ReviewerConfig, from_env() |
models.py |
MultiModelClient, ModelResponse, _sanitize_error(), REVIEW_SYSTEM_PROMPT |
reviewer.py |
CodeReviewAgent, ReviewResult, _RateLimiter, _finding_fingerprint(), to_sarif() |
rules.py |
RuleFinding, 37 _check_*() functions, ALL_RULES, run_static_checks() |
server.py |
7 @mcp.tool() functions, _JsonFormatter, main() |
The REVIEW_SYSTEM_PROMPT in models.py covers 8 categories with ~80 specific rules:
- Security — DataClassification, encryption, permissions, PII
- Performance — Partial records, FindSet/FindFirst, CalcFields, indexing, SIFT
- Best Practices — Naming, readability, unused variables, dead code, namespaces
- Localizability — Labels, ToolTips, Captions, OptionCaption, placeholders
- Upgrade & Compatibility — Breaking changes, obsolete lifecycle, public API stability
- Error Handling — Return checks, TESTFIELD, COMMIT, AssertError
- Events & Extensibility — Local subscribers, CompanyOpen, affixes, integration events
- PTE Compliance — ID ranges, ApplicationArea, permission sets, compilation target
| Variable | Description |
|---|---|
OPENAI_API_KEY |
OpenAI API key |
OPENAI_MODEL |
Model name (default: gpt-4o) |
OPENAI_BASE_URL |
Custom base URL (for proxies/compat APIs) |
OPENAI_TEMPERATURE |
Sampling temperature (default: 0.2) |
OPENAI_MAX_TOKENS |
Max response tokens (default: 4096) |
AZURE_OPENAI_API_KEY |
Azure OpenAI API key |
AZURE_OPENAI_ENDPOINT |
Azure resource endpoint URL |
AZURE_OPENAI_DEPLOYMENT |
Deployment name (default: gpt-4o) |
AZURE_OPENAI_API_VERSION |
API version (default: 2024-02-01) |
ANTHROPIC_API_KEY |
Anthropic API key |
ANTHROPIC_MODEL |
Model name (default: claude-sonnet-4-20250514) |
OLLAMA_MODEL |
Ollama model name (e.g., codellama:13b) |
OLLAMA_BASE_URL |
Ollama server URL (default: http://localhost:11434) |
| Variable | Description | Default |
|---|---|---|
REVIEWER_CONSENSUS_THRESHOLD |
Warning finding vote threshold | 0.6 |
REVIEWER_CONSENSUS_THRESHOLD_CRITICAL |
Critical finding vote threshold | 0.4 |
REVIEWER_CONSENSUS_THRESHOLD_INFO |
Info finding vote threshold | 0.8 |
REVIEWER_MAX_FILE_SIZE_KB |
Max file size in KB | 500 |
REVIEWER_MAX_INPUT_BYTES |
Hard input size limit | 512000 |
REVIEWER_TIMEOUT |
LLM request timeout (seconds) | 120 |
REVIEWER_DISABLED_RULES |
Comma-separated rule IDs to suppress | (none) |
REVIEWER_WORKSPACE_ROOT |
Restrict file access to directory | (none) |
REVIEWER_RATE_LIMIT |
Max reviews per minute | 30 |
REVIEWER_RETRY_ATTEMPTS |
Max LLM retry attempts | 3 |
REVIEWER_RETRY_BACKOFF |
Exponential backoff base (seconds) | 1.0 |
REVIEWER_LOG_FORMAT |
"text" or "json" |
text |
| HTTP Status | Retried? | Backoff |
|---|---|---|
| 429 (Rate Limit) | Yes | 1s, 2s, 4s |
| 500 (Server Error) | Yes | 1s, 2s, 4s |
| 502 (Bad Gateway) | Yes | 1s, 2s, 4s |
| 503 (Unavailable) | Yes | 1s, 2s, 4s |
| 504 (Timeout) | Yes | 1s, 2s, 4s |
| 401/403 (Auth) | No | Immediate fail |
| Network errors | Yes | 1s, 2s, 4s |
Formula: wait = retry_backoff_base * 2^attempt
Shared httpx.AsyncClient across all LLM calls:
- Max 10 concurrent connections, 5 keep-alive
- 10s connect timeout, configurable request timeout
{
"rule_id": "AA0242",
"category": "Performance",
"severity": "warning",
"line": 42,
"code_snippet": "if Customer.FindSet() then begin",
"message": "FindSet/FindFirst without SetLoadFields (AA0242)...",
"suggestion": "Add Rec.SetLoadFields(Field1, Field2) before the Find call.",
"source": "static",
"consensus_votes": 3,
"consensus_total": 3,
"doc_reference": "https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/analyzers/codecop-aa0242"
}- CodeCop Analyzer Rules
- AppSourceCop Analyzer Rules
- PerTenantExtensionCop Analyzer Rules
- Performance for Developers
- AL Formatter
- Using Partial Records
- Table Keys and Performance
- Events in AL
- API Page Type
- DataAccessIntent Property
- SumIndexField Technology (SIFT)
- Tri-state Locking
- Page Background Tasks
- OData Query Performance
- Ruleset for Code Analysis Tools
Built for Business Central AL development. All rules and standards based on Microsoft Dynamics 365 Business Central Developer Documentation.