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
26 changes: 14 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ _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:** `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:** `execute_anonymous` now reports `succeeded` where it reported `success`, and states `outputDirCreated` in place of the prose tip about `.gitignore`, so the response carries facts alone ([#109])
- **Breaking:** rename every tool, so the name says which server it belongs to and what comes back: `get_apex_log_summary` is `apexlog_get_summary`, `analyze_apex_log_performance` is `apexlog_list_slow_operations`, `find_performance_bottlenecks` is `apexlog_list_limit_risks`, and `execute_anonymous` is `apexlog_execute_anonymous`. Unprefixed names collide between servers, and a verb that names the work rather than the result cannot be told apart from another tool's ([#107])
- **Breaking:** refuse `apexlog_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:** `apexlog_list_slow_operations` 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:** `apexlog_list_limit_risks` 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 `apexlog_get_summary`, and the prose `summary` from `apexlog_list_slow_operations` in favour of a scalar share of the runtime ([#86], [#108])
- **Breaking:** `apexlog_get_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:** `apexlog_execute_anonymous` now reports `succeeded` where it reported `success`, and states `outputDirCreated` in place of the prose tip about `.gitignore`, so the response carries facts alone ([#109])
- **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% ([#86], [#108]) and `execute_anonymous` by 30% ([#86], [#109]). `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])
- Make the `apexlog_execute_anonymous` tool always discoverable, so agents can find it without server flags ([#52])
- Reduce every tool response with no fact lost: `apexlog_list_slow_operations` by 33% ([#86], [#108]) and `apexlog_execute_anonymous` by 30% ([#86], [#109]). `apexlog_get_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%: `apexlog_execute_anonymous` by 49% and `apexlog_list_limit_risks` by 25% ([#87], [#108]). `apexlog_list_slow_operations` 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 @@ -37,10 +38,10 @@ _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])
- 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])
- Declare `apexlog_execute_anonymous` destructive, so clients stop treating it as safe to run unprompted ([#52])
- Warn when a caller-given `apexlog_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])
- Return an absolute `filePath` from `apexlog_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])

Expand All @@ -63,5 +64,6 @@ _If you are upgrading from 1.x: please see [Migrating from 1.x](README.md#migrat
[#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
[#107]: https://github.com/certinia/debug-log-analyzer-mcp/issues/107
[#108]: https://github.com/certinia/debug-log-analyzer-mcp/issues/108
[#109]: https://github.com/certinia/debug-log-analyzer-mcp/issues/109
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pnpm start

- **src/index.ts**: CLI entry point (`bin`) — parses flags, constructs and runs the server
- **src/server.ts**: Main MCP server implementation (`ApexLogServer` class, `parseServerConfig`)
- Implements 4 MCP tools: `analyze_apex_log_performance`, `get_apex_log_summary`, `find_performance_bottlenecks`, `execute_anonymous`
- Implements 4 MCP tools: `apexlog_list_slow_operations`, `apexlog_get_summary`, `apexlog_list_limit_risks`, `apexlog_execute_anonymous`
- Uses stdio transport for communication
- Handles file validation, log parsing, analysis, and anonymous Apex execution

Expand Down Expand Up @@ -96,7 +96,7 @@ Every tool, parameter and response field follows the rules in [DEVELOPING.md](DE

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` 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` states `outputDirCreated` rather than advising the caller to write a `.gitignore`.
Concretely: `apexlog_list_slow_operations` returns no prose `summary` and no `recommendations` — its one unique fact beside the table is the scalar `returnedSelfPercentage`; `apexlog_get_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`; `apexlog_list_limit_risks` 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; `apexlog_execute_anonymous` states `outputDirCreated` rather than advising the caller to write a `.gitignore`.

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.

Expand All @@ -105,6 +105,6 @@ The same rule governs the **tool definitions**, which every client loads on ever
`outputSchema`/`structuredContent` are deliberately not implemented — the MCP spec asks for the payload to also be serialized into a text block, which would send it twice. Tracked separately.
Anonymous execution (4) accepts multi-line strings containing Apex, saves the resulting debug log to a local file (default: `.apex-log-mcp/` in the project root), and returns a summary with the file path. The `outputDir` parameter overrides the default save location. It supports an optional `debugLevel` parameter to configure trace flag log levels per category or set all categories at once. The response includes the org alias alongside the username when available, plus the detected org type.

`execute_anonymous` is **always registered** so agents can discover it; authorization happens per call in `src/policy/orgExecutionPolicy.ts`. `src/salesforce/orgClassification.ts` identifies the target org (`sandbox`, `scratch`, `trial`, `developer`, `production`, or `unknown` when it cannot be queried), caching one `Organization` query per org id for the server's lifetime. Non-production orgs run silently. Production and `unknown` orgs need either the `--allow-production-orgs` flag or a per-call user confirmation via MCP elicitation; without either, the call is refused with an `isError` result whose text explains both routes. Authorization runs before any `DebugLevel` or `TraceFlag` record is created, so refused calls leave the org untouched. `--no-apex-execution` refuses every call before contacting Salesforce and marks the tool `[DISABLED on this server]` in its description. The 1.x `--allowed-orgs` flag is accepted but ignored, with a stderr deprecation warning.
`apexlog_execute_anonymous` is **always registered** so agents can discover it; authorization happens per call in `src/policy/orgExecutionPolicy.ts`. `src/salesforce/orgClassification.ts` identifies the target org (`sandbox`, `scratch`, `trial`, `developer`, `production`, or `unknown` when it cannot be queried), caching one `Organization` query per org id for the server's lifetime. Non-production orgs run silently. Production and `unknown` orgs need either the `--allow-production-orgs` flag or a per-call user confirmation via MCP elicitation; without either, the call is refused with an `isError` result whose text explains both routes. Authorization runs before any `DebugLevel` or `TraceFlag` record is created, so refused calls leave the org untouched. `--no-apex-execution` refuses every call before contacting Salesforce and marks the tool `[DISABLED on this server]` in its description. The 1.x `--allowed-orgs` flag is accepted but ignored, with a stderr deprecation warning.

`src/index.ts` stays the `bin` entry point (`dist/index.js`) and does nothing but parse flags and start the server. `src/server.ts` holds `ApexLogServer` and `parseServerConfig` and is free of import side effects, so tests can import it without spawning a server or parsing the test runner's own argv.
4 changes: 2 additions & 2 deletions DEVELOPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ To get started, clone this repository and install the necessary dependencies.
pnpm i
```

4. **Optional: Set a default org (execute_anonymous tool only)**
4. **Optional: Set a default org (apexlog_execute_anonymous tool only)**

The execute anonymous Apex tool requires a default org to be set using the Salesforce CLI. To do this in a repository that has no `sfdx-project.json` like this one, set your default org globally:

Expand Down Expand Up @@ -274,7 +274,7 @@ hint quietly coming back.

`destructiveHint` and `idempotentHint` are defined as meaningful only when `readOnlyHint` is false, so
the three read-only tools declare `readOnlyHint: true` and `openWorldHint: false` and nothing more —
both differ from the spec default, and both say something. `execute_anonymous` keeps all four hints; it
both differ from the spec default, and both say something. `apexlog_execute_anonymous` keeps all four hints; it
is the one tool where a client that misreads a default runs Apex against an org.

### Know the floor
Expand Down
Loading