Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ It is deliberately **thin**: Microsoft's `almcp` already compiles, runs diagnost

1. `Program.cs` calls `BcToolsLocator.ResolveAndRegister()` to find the tools directory and register an `AssemblyLoadContext` resolver. 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.
2. `McpHost.RunAsync()` is marked `[NoInlining]` to enforce that ordering, then builds the host, registers DI services, and starts the MCP stdio transport.
3. `AlMcpProxyStartup` (an `IHostedService`) launches `almcp` as 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.Ready` is the signal everything else waits on: `tools/list` gives it 10s and otherwise answers with the native tools plus a one-shot `notifications/tools/list_changed`; an `al_*` call that arrives early parks on `Ready` instead of failing.
3. `AlMcpProxyStartup` (an `IHostedService`) launches `almcp` as a child process on a free localhost port and caches its tool list — **on a background task**, so the stdio server and the five native tools are up immediately no matter how long the child takes. `AlMcpProxy.Ready` is the signal everything else waits on: `tools/list` gives it 10s and otherwise answers with the native tools plus a one-shot `notifications/tools/list_changed`; an `al_*` call that arrives early parks on `Ready` instead of failing.

### Key layers

- **Tools/** — MCP tool endpoints, annotated `[McpServerToolType]` / `[McpServerTool]`, auto-discovered via `WithToolsFromAssembly()`. Four native tools: `list_rules`, `get_fixes`, `apply_fix`, `apply_fix_all`. The proxied `al_*` tools are served by the dynamic list/call handlers in `McpHost`, not by classes here.
- **Tools/** — MCP tool endpoints, annotated `[McpServerToolType]` / `[McpServerTool]`, auto-discovered via `WithToolsFromAssembly()`. Five native tools: `list_rules`, `get_fixes`, `apply_fix`, `apply_fix_all`, `analyze`. The proxied `al_*` tools are served by the dynamic list/call handlers in `McpHost`, not by classes here.
- **Services/**, all singletons registered in `McpHost`:
- `BcToolsLocator` — the single runtime lookup. Finds the one directory holding both `Microsoft.Dynamics.Nav.*.dll` and `almcp` (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. `AlMcpLaunch` describes how to start the child: native `almcp.exe` on Windows, `dotnet almcp.dll` on Linux/macOS (the nupkg ships no extension-less launcher).
- `WorkspaceStartupResolver` — discovers AL projects (mirrors `almcp`'s own `DiscoverProjectPaths`: downward scan for `app.json`, depth 4, standard exclusions) and composes the child `almcp`'s `--projects` / `--codeanalyzers` / `--rulesetpath` / `--packagecachepath` args. `almcp` in MCP mode never reads `.vscode/settings.json` and has no per-call analyzer, ruleset or package-cache parameter, so this bridge at launch is the only thing keeping `al_compile` and our fix tools in agreement (`ProjectLoader` reads the same `al.packageCachePath` for the in-process compilation).
Expand All @@ -66,8 +66,10 @@ When passing analyzers to the child `almcp`, their sibling dependencies must tra
- 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_fix` writes to disk and reloads the project session; `apply_fix_all` does the same across every occurrence of a rule (unless `dryRun`); the other two are read-only.
- `al_compile` defaults to `onlyErrors: true` while nearly every ALCops rule is a warning — callers must pass `onlyErrors: false`. This is documented rather than patched, because `ForwardAsync` stays a generic passthrough.
- After `apply_fix` / `apply_fix_all`, verify with `al_compile` (`onlyErrors: false`), not `al_getdiagnostics`. almcp's `ProjectWatcher` (`FileSystemWatcher`) re-reads changed `.al` files, and `al_compile` awaits `WaitForProcessingAsync` before compiling, so it normally picks up on-disk changes before the compile starts. The gate starts signalled and has no debounce, so on slow file systems or right after a large `apply_fix_all` a second `al_compile` may be needed if the watcher has not yet delivered the change notification. `al_getdiagnostics` returns cached compilation results without re-analyzing and will report stale diagnostics. `al_build` does not await the watcher at all.
- `al_compile` defaults to `onlyErrors: true` while nearly every ALCops rule is a warning — callers must pass `options.onlyErrors: false` (the flag lives inside almcp's `options` object; a top-level `onlyErrors` is ignored, and omitting `options` entirely also yields `onlyErrors: false` with no diagnostics cap). This is documented rather than patched, because `ForwardAsync` stays a generic passthrough.
- After `apply_fix` / `apply_fix_all`, verify with `analyze` (preferred) or `al_compile` (`options.onlyErrors: false`), not `al_getdiagnostics`. almcp's `ProjectWatcher` (`FileSystemWatcher`) re-reads changed `.al` files, and `al_compile` awaits `WaitForProcessingAsync` before compiling, so it normally picks up on-disk changes before the compile starts. The gate starts signalled and has no debounce, so on slow file systems or right after a large `apply_fix_all` a second `al_compile` may be needed if the watcher has not yet delivered the change notification. `al_getdiagnostics` returns cached compilation results without re-analyzing and will report stale diagnostics. `al_build` does not await the watcher at all.
- `analyze` is the one native tool that goes through `AlMcpProxy.ForwardAsync` (`al_compile` with nested `options.onlyErrors=false`, `enableCodeAnalysis=true`, `maxDiagnosticsPerCompilation=int.MaxValue`, never `codeAnalyzers`); it resolves the proxy via `IServiceProvider` because under `--no-proxy` the type is unregistered and the SDK would otherwise expose it as a tool argument; it returns `{error:"ProxyUnavailable"}` in that case.
- `analyze` inherits the FileSystemWatcher staleness caveat after `apply_fix` / `apply_fix_all`.

## Conventions

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Add to your `.mcp.json` (Claude Code) or `claude_desktop_config.json` (Claude De
| Linux | `dotnet almcp.dll` | The nupkg has no extension-less launcher; the server falls back to the dotnet host automatically. |
| macOS | `dotnet almcp.dll` | Same as Linux. |

The native tools (`list_rules`, `get_fixes`, `apply_fix`, `apply_fix_all`) work on every OS regardless of `almcp` availability.
The native tools (`list_rules`, `get_fixes`, `apply_fix`, `apply_fix_all`) work on every OS regardless of `almcp` availability. `analyze` wraps `al_compile` and therefore needs `almcp`.

## Tools

Expand All @@ -55,6 +55,7 @@ The native tools (`list_rules`, `get_fixes`, `apply_fix`, `apply_fix_all`) work
| `get_fixes` | Get available code fixes for a specific diagnostic at a location. |
| `apply_fix` | Apply a code fix to resolve a diagnostic. Writes the fixed content directly to the file on disk. |
| `apply_fix_all` | Apply a code fix to every occurrence of a diagnostic rule across a project or a single file (like VS Code's "Fix all in workspace"). Writes to disk unless `dryRun` is set. |
| `analyze` | Compile with all configured analyzers and return structured cop + compiler diagnostics (analyzer, hasFix, filters, summary). Wraps `al_compile` with `onlyErrors: false`; needs `almcp`. |

### Proxied from Microsoft's `almcp`

Expand All @@ -64,11 +65,11 @@ The native tools (`list_rules`, `get_fixes`, `apply_fix`, `apply_fix_all`) work

Pass `--no-proxy` to serve only the native tools. Use it when your agent already registers Microsoft's `almcp` itself, so the `al_*` tools don't show up twice.

> **`al_compile` defaults to `onlyErrors: true`.** Nearly every ALCops rule is a *warning*, so pass `onlyErrors: false` or you will see no cop diagnostics at all.
> **`al_compile` defaults to `onlyErrors: true`.** Nearly every ALCops rule is a *warning*, so pass `options: { onlyErrors: false }` or you will see no cop diagnostics at all. The flag lives inside the `options` object; a top-level `onlyErrors` is ignored. The native `analyze` tool does this for you and adds filtering, sorting and `hasFix` metadata.

### Verifying a fix

After `apply_fix` or `apply_fix_all`, use `al_compile` with `onlyErrors: false` to confirm the diagnostic is gone. `al_compile` awaits almcp's internal file watcher, which normally sees the write before the compile starts. The watcher gate is not debounced, so on slow file systems or right after a large `apply_fix_all` a second `al_compile` may be needed. Do **not** use `al_getdiagnostics` for this: it returns cached compilation results rather than re-analyzing, and will report stale (or empty) diagnostics. `al_build` does not await the watcher. Only restarting the server gives a fully fresh almcp workspace.
After `apply_fix` or `apply_fix_all`, call `analyze` (preferred) or `al_compile` with `options.onlyErrors: false` to confirm the diagnostic is gone. Both await almcp's internal file watcher, which normally sees the write before the compile starts. The watcher gate is not debounced, so on slow file systems or right after a large `apply_fix_all` a second call may be needed. Do **not** use `al_getdiagnostics` for this: it returns cached compilation results rather than re-analyzing, and will report stale (or empty) diagnostics. `al_build` does not await the watcher. Only restarting the server gives a fully fresh almcp workspace.

## Analyzers

Expand Down
24 changes: 24 additions & 0 deletions src/ALCops.Mcp/Models/AnalyzeResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace ALCops.Mcp.Models;

public record AnalyzeDiagnostic(
string? FilePath,
int? Line,
int? Column,
string Id,
string Severity,
string Message,
string Analyzer,
bool HasFix);

public record AnalyzeSummary(
IReadOnlyDictionary<string, int> BySeverity,
IReadOnlyDictionary<string, int> ByAnalyzer);

public record AnalyzeResult(
string Project,
int Count,
int TotalCount,
bool Truncated,
AnalyzeSummary Summary,
IReadOnlyList<AnalyzeDiagnostic> Diagnostics,
IReadOnlyList<string>? Warnings);
4 changes: 2 additions & 2 deletions src/ALCops.Mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ Add to your `.mcp.json` (Claude Code) or `claude_desktop_config.json` (Claude De

## Tools

**Native:** `list_rules`, `get_fixes`, `apply_fix`, `apply_fix_all` — code fixes and rule discovery, which Microsoft's `almcp` does not provide.
**Native:** `list_rules`, `get_fixes`, `apply_fix`, `apply_fix_all`, `analyze` — code fixes, rule discovery, and structured diagnostics with filtering. `analyze` wraps `al_compile` (needs `almcp`); the others work without it.

**Proxied from `almcp`:** `al_compile`, `al_build`, `al_getdiagnostics`, `al_downloadsymbols`, `al_symbolsearch`, `al_publish`, `al_run_tests` and the rest of the `al_*` set. Pass `--no-proxy` to suppress these when your agent already registers `almcp` itself.

> **`al_compile` defaults to `onlyErrors: true`.** Nearly every ALCops rule is a *warning*, so pass `onlyErrors: false` or you will see no cop diagnostics at all.
> **`al_compile` defaults to `onlyErrors: true`.** Nearly every ALCops rule is a *warning*, so pass `options: { onlyErrors: false }` (the flag lives inside the `options` object; a top-level `onlyErrors` is ignored) or you will see no cop diagnostics at all. The native `analyze` tool does this for you.

## Analyzers

Expand Down
Loading
Loading