Skip to content
Merged
21 changes: 15 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ _If you are upgrading from 1.x: please see [Migrating from 1.x](README.md#migrat
### Changed

- **Breaking:** refuse `execute_anonymous` against production orgs, and orgs whose type cannot be read, unless the run is confirmed via [MCP elicitation](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation) or `--allow-production-orgs` is set ([#52])
- **Breaking:** drop `file` from `get_apex_log_summary`, and the prose `summary` from `analyze_apex_log_performance` in favour of the scalar `topMethodsSelfPercentage` ([#86])
- **Breaking:** report governor limits as a flat `{name, used, limit}` table, and include the limits at zero, so a caller can tell "no DML ran" from "DML was never read" ([#86])
- **Breaking:** `analyze_apex_log_performance` now ranks every timed operation by self time, not methods alone: code units, managed packages, methods, system methods, queries, searches, DML, flows and workflows, in one table of `{kind, name, namespace, lineNumber, callCount, durationTotalMs, durationSelfMs, selfPercentage, soqlCount, dmlCount, soslCount, rowCount, thrownCount}` rows. `slowestMethods`, `totalMethods`, `totalExecutionTime`, `topMethodsSelfPercentage` and `recommendations` are gone, replaced by `operations`, `durationTotalMs` and `returnedSelfPercentage`. The `topMethods` and `minDuration` parameters are now `limit` and `minSelfMs`, beside new `kind`, `namespace` and `groupBy` parameters that select and fold the rows ([#108])
- **Breaking:** `find_performance_bottlenecks` now returns one table of the governor limits at risk — `{limit, used, max, usedPercentage}` rows, worst first — beside the `threshold` that selected them. The `cpuBottlenecks`, `databaseBottlenecks`, `methodBottlenecks` and `governorLimitWarnings` sections, the `note`, and the `analysisType` parameter are gone; a new `threshold` parameter sets where a limit becomes worth reporting. All thirteen limits are covered, where the sections covered six, and the response costs 78% less ([#108])
- **Breaking:** drop `file` from `get_apex_log_summary`, and the prose `summary` from `analyze_apex_log_performance` in favour of a scalar share of the runtime ([#86], [#108])
- **Breaking:** `get_apex_log_summary` now reports where the time went and what each namespace consumed. `timeByKind` gives `{kind, logCategory, operationCount, durationSelfMs, selfPercentage}` for every kind of operation, and `limitsByNamespace` gives `{namespace, limit, used}` for each limit a namespace consumed, so a managed package that spends your CPU time is visible. `totalMethods`, `totalSOQLQueries`, `totalDMLOperations`, `totalSOQLRows` and `totalDMLRows` are gone, and searches are covered for the first time. `size`, `totalExecutionTime` and `parsingErrors` are now `fileSizeBytes`, `durationTotalMs` and `parsingErrorCount`, beside a new `truncated`. A `debugLevels` row names its `logCategory`, not its `category`, which is the name `timeByKind` uses for the same fact ([#62], [#108])
- **Breaking:** report governor limits as a flat `{limit, used, max}` table, and include the limits at zero, so a caller can tell "no DML ran" from "DML was never read" ([#86], [#62])
- Make the `execute_anonymous` tool always discoverable, so agents can find it without server flags ([#52])
- Reduce every tool response with no fact lost: `analyze_apex_log_performance` by 33%, `execute_anonymous` by 30% after the first run, `get_apex_log_summary` by 27% and `find_performance_bottlenecks` by 5% ([#86])
- Reduce the standing cost of having the server connected by 31%, with no tool renamed, no parameter removed and no response changed: `execute_anonymous` by 49%, `find_performance_bottlenecks` by 12%, `get_apex_log_summary` by 11% and `analyze_apex_log_performance` by 4% ([#87])
- Reduce every tool response with no fact lost: `analyze_apex_log_performance` by 33% ([#86], [#108]) and `execute_anonymous` by 30% after the first run ([#86]). `get_apex_log_summary` costs 16% more on a log that uses its limits, for the two tables it gained ([#62])
- Reduce the standing cost of having the server connected by 26%: `execute_anonymous` by 49% and `find_performance_bottlenecks` by 25% ([#87], [#108]). `analyze_apex_log_performance` costs 32% more, for the five parameters that select what it ranks ([#108])
- Parse a log once rather than once per tool, cached by path, inode, size, modification time and change time, so a summary followed by a deeper tool no longer reads and parses the file again. The parse is dropped after five minutes unused, so a large log is not held for the life of the session ([#88])

### Added
Expand All @@ -34,8 +37,11 @@ _If you are upgrading from 1.x: please see [Migrating from 1.x](README.md#migrat
### Fixed

- Declare `execute_anonymous` destructive, so clients stop treating it as safe to run unprompted ([#52])
- Stop `analyze_apex_log_performance` reporting that performance looks good on a log that exhausted the CPU limit ([#86])
- Report the same `totalMethods` from all three analysis tools on an unfiltered call; `get_apex_log_summary` did not count entry points, so it reported fewer methods than the other two ([#88])
- Warn when a caller-given `execute_anonymous` `outputDir` resolves outside every root the client declared. The log is still written, and the response names where it went ([#109])
- Close cleanly on `SIGTERM`, so a supervised restart or a container stop no longer kills the server mid-shutdown ([#109])
- Return an absolute `filePath` from `execute_anonymous`, so the path it hands back is one the analysis tools accept. A relative `outputDir` now anchors to the project root, the same base the default uses ([#109])
- Refuse a relative `logFilePath` instead of resolving it against the server's working directory, which is where the client spawned the server and not where the caller is ([#109])
- Name the real cause when a log file cannot be opened. A permission error, a directory in place of a file, or an exhausted descriptor table were all reported as "Log file not found", sending the caller to look for a file that was there ([#109])

## [1.0.0] - 2026-03-20

Expand All @@ -52,6 +58,9 @@ _If you are upgrading from 1.x: please see [Migrating from 1.x](README.md#migrat
<!-- Unreleased -->

[#52]: https://github.com/certinia/debug-log-analyzer-mcp/issues/52
[#62]: https://github.com/certinia/debug-log-analyzer-mcp/issues/62
[#86]: https://github.com/certinia/debug-log-analyzer-mcp/issues/86
[#87]: https://github.com/certinia/debug-log-analyzer-mcp/issues/87
[#88]: https://github.com/certinia/debug-log-analyzer-mcp/issues/88
[#108]: https://github.com/certinia/debug-log-analyzer-mcp/issues/108
[#109]: https://github.com/certinia/debug-log-analyzer-mcp/issues/109
18 changes: 11 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ pnpm start
- Uses stdio transport for communication
- Handles file validation, log parsing, analysis, and anonymous Apex execution

- **src/tools/responseShaping.ts**: Shared helpers for keeping responses lean — `omitEmpty`, `toLimitRows`, `roundMs`, `roundPercent`, `NS_TO_MS`
- **src/tools/responseShaping.ts**: Shared helpers for keeping responses lean — `omitEmpty`, `toLimitRows`, `toNamespaceLimitRows`, `roundMs`, `roundPercent`, `NS_TO_MS`

- **src/tools/apexLogSource.ts**: The one way the three analysis tools get a log — `loadApexLog` (reads and parses, caching the last parse by path and a fingerprint of everything a stat can see — inode, size, and modification and change time in nanoseconds, so a `cp -p` that keeps the modification time still misses; the file is opened once and both the stat and the read go to that handle, so nothing can put a different file at the path between the two; the slot holds the in-flight promise, so concurrent callers share one parse and a failed read is not kept; it is dropped five minutes after its last use, on an `unref`ed timer, because a parsed log holds four to five times the size of the file), `isMethodNode` (the one method test, so the tools agree on `totalMethods`) and `walkLog`
- **src/tools/apexLogSource.ts**: The one way the three analysis tools get a log — `loadApexLog` (reads and parses, caching the last parse by path and a fingerprint of everything a stat can see — inode, size, and modification and change time in nanoseconds, so a `cp -p` that keeps the modification time still misses; the file is opened once and both the stat and the read go to that handle, so nothing can put a different file at the path between the two; the slot holds the in-flight promise, so concurrent callers share one parse and a failed read is not kept; it is dropped five minutes after its last use, on an `unref`ed timer, because a parsed log holds four to five times the size of the file) and `walkLog`

- **src/ApexLogParser.ts**: Complex log parsing engine (33k+ tokens)
- Exports `parse()` function and `ApexLogParser` class
Expand All @@ -45,7 +45,7 @@ pnpm start

- `ApexLog`: Root log structure with duration, governor limits, namespaces
- `LogLine`: Individual log entries with hierarchical relationships
- `SlowMethod`: Performance analysis result with timing and resource usage
- `Operation`: One timed thing the transaction did, with its timing and resource usage
- `GovernorLimits`: Salesforce platform limits tracking

### MCP Integration
Expand Down Expand Up @@ -81,20 +81,24 @@ dist/ # Compiled JavaScript output

The server provides four main capabilities:

1. **Performance Analysis**: Identifies slowest methods with detailed metrics
1. **Performance Analysis**: Ranks every timed operation by self time, with detailed metrics
2. **Log Summary**: High-level execution statistics and governor limit usage
3. **Bottleneck Detection**: Analyzes CPU, database, and method performance patterns
4. **Execute Anonymous**: Executes anonymous Apex code snippets, saves the debug log to a file, and returns a summary with the file path

Log analysis tools (1-3) accept absolute file paths to `.log` files and return structured JSON for AI processing.

## Naming

Every tool, parameter and response field follows the rules in [DEVELOPING.md](DEVELOPING.md#-naming-tools-and-fields) — read them before you add or rename one. In short: prefix every tool `apexlog_`; the verb states the shape of the result (`get_` one, `list_` many, `search_` many matched to a caller query, `create`/`update`/`delete`/`write_` one resource written, `execute`/`run_` an effect outside the server) and the noun states what the result is; `analyze`, `process`, `handle`, `manage`, `find`, `detect`, `check` and `fetch` are banned; fields name the fact and not the calculation, carry their unit (`durationSelfMs`, `fileSizeBytes`), use `total` only for "including children" and `self` only for "excluding them" (so `durationTotalMs` names a log's duration and a row's alike), keep one name per fact across all tools, count as `<noun>Count`, fold acronyms in lowerCamel, and state booleans as bare adjectives (`truncated`).

## Response Shaping

Responses are TOON-encoded and deliberately lean, but the saving comes from shape, never from dropping a fact — see the conventions in [DEVELOPING.md](DEVELOPING.md#️-shaping-tool-responses) and the helpers in `src/tools/responseShaping.ts`. In short: restructure before you delete (flatten nested objects into TOON tables — `toLimitRows` is the worked example, 45% cheaper than the nested form and still complete); always report a fixed-schema field even at zero, because an absent count cannot be told apart from one that was never parsed; use `omitEmpty` **only** for occurrence lists, where absence unambiguously means nothing happened; never report the same figure twice; never echo the caller's input back; round durations and percentages (`roundMs`/`roundPercent`); and keep every row of a table on the same key set so TOON keeps its one-header-plus-one-line-per-row form.
Responses are TOON-encoded and deliberately lean, but the saving comes from shape, never from dropping a fact — see the conventions in [DEVELOPING.md](DEVELOPING.md#️-shaping-tool-responses) and the helpers in `src/tools/responseShaping.ts`. In short: restructure before you delete (flatten nested objects into TOON tables — `toLimitRows` is the worked example, 45% cheaper than the nested form and still complete); always report a fixed-schema field even at zero, because an absent count cannot be told apart from one that was never parsed; use `omitEmpty` **only** for occurrence lists, where absence unambiguously means nothing happened; never report the same figure twice; never state what the caller can derive from the numbers beside it; never echo the caller's input back; round durations and percentages (`roundMs`/`roundPercent`); and keep every row of a table on the same key set so TOON keeps its one-header-plus-one-line-per-row form.

Concretely: `analyze_apex_log_performance` returns no prose `summary` — its one unique fact is the scalar `topMethodsSelfPercentage` — and omits `recommendations` only when nothing stands out; `get_apex_log_summary` returns no `file`, all thirteen governor limits as `{name, used, limit}` rows, the full `debugLevels` list, and omits only `logIssues`; `find_performance_bottlenecks` excludes from `governorLimitWarnings` any limit already detailed by a dedicated section; `execute_anonymous` emits the `.gitignore` tip only when it created the output directory.
Concretely: `analyze_apex_log_performance` returns no prose `summary` and no `recommendations` — its one unique fact beside the table is the scalar `returnedSelfPercentage`; `get_apex_log_summary` returns no `file`, all thirteen governor limits as `{limit, used, max}` rows, the limits each namespace consumed as `{namespace, limit, used}` rows, one `timeByKind` row per operation kind carrying the trace category that decides whether the kind was logged, the full `debugLevels` list, and omits only `logIssues`; `find_performance_bottlenecks` returns one `atRisk` table of `{limit, used, max, usedPercentage}` rows and reports it even when empty, beside the `threshold` that selected them, because a selection with no stated cutoff cannot be read; `execute_anonymous` emits the `.gitignore` tip only when it created the output directory.

The same rule governs the **tool definitions**, which every client loads on every turn whether a tool is called or not (`tools/list` is ~1,090 tokens for the four tools). Say each thing once: an enum already lists its values, so a `.describe()` must not repeat them — this is why `debugLevel` is one `z.partialRecord(z.enum(TRACE_CATEGORIES), z.enum(LOG_LEVELS))` with a single description rather than ten per-category properties, and why `LOG_LEVELS` and `TRACE_CATEGORIES` live only in `src/salesforce/debugLevels.ts`. A description earns its tokens only if the agent acts on it: response-shaping policy is a contributor fact and belongs here and in DEVELOPING.md, not on the wire. Set `title` at the top level only — `annotations.title` is a duplicate alias and is sent twice. Anything true of every tool (durations are milliseconds, which tool to start with) goes in the server `instructions` once.
The same rule governs the **tool definitions**, which every client loads on every turn whether a tool is called or not (`tools/list` is ~1,020 tokens for the four tools). Say each thing once: an enum already lists its values, so a `.describe()` must not repeat them — this is why `debugLevel` is one `z.partialRecord(z.enum(TRACE_CATEGORIES), z.enum(LOG_LEVELS))` with a single description rather than ten per-category properties, and why `LOG_LEVELS` and `TRACE_CATEGORIES` live only in `src/salesforce/debugLevels.ts`. A description earns its tokens only if the agent acts on it: response-shaping policy is a contributor fact and belongs here and in DEVELOPING.md, not on the wire. Set `title` at the top level only — `annotations.title` is a duplicate alias and is sent twice. Anything true of every tool (durations are milliseconds, which tool to start with) goes in the server `instructions` once.

`pnpm run eval` (`scripts/eval.mjs`, wired into CI) is the gate for response-shape changes: it drives the built server over stdio against `tests/eval/fixtures/` and checks answerability, no duplication, a token budget and golden files. Once per run it also budgets the tool definitions — see [Shaping Tool Definitions](DEVELOPING.md#️-shaping-tool-definitions) — and regenerates both token cost tables in `README.md`, between the `<!-- token-cost-definitions -->` and `<!-- token-cost-answers -->` markers, so any change that moves a published figure fails until the README is regenerated with it. The jest suite cannot do this — `moduleNameMapper` swaps `@toon-format/toon` for a JSON stand-in, so it never sees the real encoding. Re-record goldens with `pnpm run build && pnpm run eval:update` and read the diff.

Expand Down
Loading