Imported from ALCops/mcp-server (
AGENTS.md). Install upstream withnpx skills add ALCops/mcp-server. Copyright stays with the author.
ALCops MCP Server — Agent Instructions
Build
# Build (BC DevTools come from restore, see below)
dotnet build --configuration Release
# Run tests
dotnet test --configuration Release
# Pack as .NET global tool
dotnet pack src/ALCops.Mcp/ALCops.Mcp.csproj --configuration Release --output ./artifacts
There are no linters in this repository.
BC DevTools dependency
The project compiles against proprietary Microsoft BC Development Tools DLLs (Microsoft.Dynamics.Nav.CodeAnalysis, .Workspaces, .Analyzers.Common).
A plain PackageReference is impossible: as of 17.0 the Microsoft.Dynamics.BusinessCentral.Development.Tools package is DotnetTool + Template only, with all payload under tools/<tfm>/any/, and NuGet rejects referencing a DotnetTool package. Instead both csproj files declare a <PackageDownload> on a pinned version — which restores the nupkg into the global packages folder without referencing it — and point three <Reference> items at $(NuGetPackageRoot)…/tools/$(BcToolsTfm)/any/.
$(BcDevToolsVersion)— the compile floor, deliberately the lowest supported stable release. Compiling against the oldest SDK and running against newer ones is what makes forward compatibility hold; CI overrides this property to run its version matrix.$(BcToolsTfm)—net10.0by default, matching our own TFM (18.x ships bothnet8.0andnet10.0payloads; the 17.x line shipped net8.0 only).srcsets<Private>false</Private>, so the proprietary DLLs never enter the build output and therefore never enter the published package. This is the redistribution guard.testssets<Private>true</Private>on purpose: the CI compatibility matrix hot-swaps those three DLLs in the prebuilt test binary to run the same tests against every supported SDK version.
At runtime BcToolsLocator finds the DLLs in the user's own toolchain. The DevTools themselves are never downloaded at runtime; only ALCops' own analyzers are provisioned from NuGet (see below).
Architecture
An MCP (Model Context Protocol) server packaged as a .NET 10 global tool (alcops-mcp), served over stdio JSON-RPC.
It is deliberately thin: Microsoft's almcp already compiles, runs diagnostics, resolves symbols, publishes, runs tests and handles translations, so all of that is proxied. What almcp has no capability for at all — code fixes (its LSP mode advertises CodeActionProvider = false) and rule enumeration — is what this server implements natively. Adding anything here that almcp already does is a regression of that design.
Startup sequence (order matters)
Program.cscallsBcToolsLocator.ResolveAndRegister()to find the tools directory and register anAssemblyLoadContextresolver. This must happen before any BC types are JIT-compiled — the DLLs are not in the output directory, so nothing can resolve them before this runs.McpHost.RunAsync()is marked[NoInlining]to enforce that ordering, then builds the host, registers DI services, and starts the MCP stdio transport.AlMcpProxyStartup(anIHostedService) launchesalmcpas a child process on a free localhost port and caches its tool list — on a background task, so the stdio server and the four native tools are up immediately no matter how long the child takes.AlMcpProxy.Readyis the signal everything else waits on:tools/listgives it 10s and otherwise answers with the native tools plus a one-shotnotifications/tools/list_changed; anal_*call that arrives early parks onReadyinstead of failing.
Key layers
- Tools/ — MCP tool endpoints, annotated
[McpServerToolType]/[McpServerTool], auto-discovered viaWithToolsFromAssembly(). Four native tools:list_rules,get_fixes,apply_fix,apply_fix_all. The proxiedal_*tools are served by the dynamic list/call handlers inMcpHost, not by classes here. - Services/, all singletons registered in
McpHost:BcToolsLocator— the single runtime lookup. Finds the one directory holding bothMicrosoft.Dynamics.Nav.*.dllandalmcp(they ship side by side). Probe order:--devtools-path→BCDEVELOPMENTTOOLSPATH→ dotnet tool store → hard error naming every probe and the install command. The AL VS Code extension is no longer probed; the dotnet tool store is the primary channel on every OS. No auto-install by design.AlMcpLaunchdescribes how to start the child: nativealmcp.exeon Windows,dotnet almcp.dllon Linux/macOS (the nupkg ships no extension-less launcher).WorkspaceStartupResolver— discovers AL projects (mirrorsalmcp's ownDiscoverProjectPaths: downward scan forapp.json, depth 4, standard exclusions) and composes the childalmcp's--projects/--codeanalyzers/--rulesetpath/--packagecachepathargs.almcpin MCP mode never reads.vscode/settings.jsonand has no per-call analyzer, ruleset or package-cache parameter, so this bridge at launch is the only thing keepingal_compileand our fix tools in agreement (ProjectLoaderreads the sameal.packageCachePathfor the in-process compilation).AlMcpProxy— child process lifecycle plus generic tool forwarding over a single long-lived MCP client that reconnects on session expiry.ForwardAsyncis a passthrough with no per-tool argument rewriting; configuration is conveyed at launch instead.ProjectAnalyzerResolver— readsal.codeAnalyzersand the ruleset (.vscode/settings.json,.AL-Go/settings.json, convention-named files) and builds anAnalyzerSet. Nothing is built in.AlcopsAnalyzerProvisioner— downloads ALCops' own analyzers from NuGet, matched to the installed DevTools TFM, and caches them under~/.alcops/analyzers/.Task<string?> Readycompletes with the provisioned folder ornull. Configured via--alcops-analyzers/ALCOPS_ANALYZERS/ALCOPS_ANALYZERS_CACHE.ExternalAnalyzerLoader— loads analyzer DLLs throughAnalyzerAssemblyLoadContext, which resolves shared types by simple name from the default context. That type sharing is what makestypeof(DiagnosticAnalyzer).IsAssignableFromwork, and therefore what makes in-process code fixes possible at all.ProjectSessionManager/ProjectLoader— caches AL project workspaces keyed by path;GetOrLoadProjectAsyncis the entry point tools use.
- Models/ — record types for tool return values, serialized with
JsonDefaults.Options(camelCase, not indented).
Analyzers are never bundled; ALCops' own analyzers are provisioned at runtime
Shipping pinned cop DLLs beside whatever Nav.CodeAnalysis the user installed is what caused AD0001 / MissingMethodException (issue #10). Microsoft cops and third-party analyzers come solely from the project's own config and the DevTools directory. ALCops.Analyzers is referenced by the test project only, so the fixtures have real cops with real code fixes to exercise; it must never move back to src.
ALCops' own analyzers are provisioned by AlcopsAnalyzerProvisioner at every startup: it detects the DevTools TFM, downloads the latest stable ALCops.Analyzers NuGet package (or uses a pinned/prerelease version per --alcops-analyzers), extracts the matching lib/<tfm>/ folder, and caches the DLLs under ~/.alcops/analyzers/<tfm>/<version>/. ExternalAnalyzerLoader.ResolveDllPath probes the provisioned folder first for ${analyzerFolder}ALCops.*.dll specs. The DevTools themselves are never downloaded at runtime.
When passing analyzers to the child almcp, their sibling dependencies must travel with them (ALCops.Common.dll, Microsoft.Dynamics.Nav.Analyzers.Common.dll): almcp resolves analyzer dependencies only among the paths it was given and does not probe the analyzer's directory. A missing one turns every rule in that assembly into an AD0001 instead of a diagnostic.
Tool patterns
- All tool methods are
static async Task<string>, receiving DI services as parameters. - Tools return JSON-serialized results. Errors are caught and returned as
{ error, message }JSON, not thrown. apply_fixwrites to disk and reloads the project session;apply_fix_alldoes the same across every occurrence of a rule (unlessdryRun); the other two are read-only.al_compiledefaults toonlyErrors: truewhile nearly every ALCops rule is a warning — callers must passonlyErrors: false. This is documented rather than patched, becauseForwardAsyncstays a generic passthrough.- After
apply_fix/apply_fix_all, verify withal_compile(onlyErrors: false), notal_getdiagnostics. almcp's child has aProjectWatcher(FileSystemWatcher) that re-reads changed.alfiles, andal_compileawaitsWaitForProcessingAsyncbefore compiling, so it picks up on-disk changes reliably.al_getdiagnosticsreturns cached compilation results without re-analyzing and will report stale diagnostics.
Conventions
- Target framework: .NET 10, C# latest, nullable enabled, implicit usings.
- Namespaces:
ALCops.Mcp.Tools,ALCops.Mcp.Services,ALCops.Mcp.Models. File-scoped namespaces throughout. - JSON serialization: Use
JsonDefaults.Options(camelCase) for tool responses. UseJsonDocumentOptionswithCommentHandling.SkipandAllowTrailingCommaswhen parsing user-facing JSON files (settings.json, rulesets). - Logging: All diagnostic output goes to stderr. Stdout is reserved for the MCP JSON-RPC protocol. Startup logs which tools directory won, which
settings.jsonwas read, and every resolved analyzer and ruleset path — a silently wrong working-directory guess is the failure mode this exists to make visible. - Versioning: GitVersion with GitHubFlow. Version is determined from git history, not hardcoded. Branches:
mainproduces alpha prereleases,release/**branches produce stable versions.