Skip to main content
Glama
certinia

Apex Log MCP Server

Official
by certinia
README.md
# Apex Log MCP Server

[![npm version](https://img.shields.io/npm/v/@certinia/apex-log-mcp)](https://www.npmjs.com/package/@certinia/apex-log-mcp)
[![CI](https://github.com/certinia/debug-log-analyzer-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/certinia/debug-log-analyzer-mcp/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
[![Node.js](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org/)

**MCP Server to Analyze Salesforce Apex debug logs from your AI assistant. Finds slow methods, governor limit risks, and where a transaction spent its time. In your org, runs anonymous Apex, fetches logs, and sets trace flags.**

<p align="center">
  <img src="https://raw.githubusercontent.com/certinia/debug-log-analyzer-mcp/main/docs/images/apex-log-mcp.png" alt="Claude analyzing an Apex debug log for performance bottlenecks and governor limit concerns" width="800" />
</p>

Instead of scrolling thousands of log lines, ask what's slow and why. Uses the same [Apex log parser](https://github.com/apex-dev-tools/apex-log-parser) as the [Apex Log Analyzer VS Code extension](https://github.com/certinia/debug-log-analyzer).

## Quick Start

Requires [Node.js](https://nodejs.org/) 22 or later. Add this to your MCP client config (`claude_desktop_config.json`, VS Code `mcp.json`, and so on):

```json
{
  "mcpServers": {
    "apex-log-mcp": {
      "command": "npx",
      "args": ["-y", "@certinia/apex-log-mcp"]
    }
  }
}
```

In VS Code `mcp.json`, use `"servers"` in place of `"mcpServers"`.

Then ask your assistant to analyze a log. The org tools - `apexlog_execute_anonymous`, `apexlog_list_org_logs`, `apexlog_get_org_logs` and `apexlog_create_trace_flag` - also need an org authenticated with the [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli).

## Example Prompts

- "Give me a summary of this debug log"
- "Show me the 5 slowest methods in the default namespace"
- "Are we approaching any governor limits in this transaction?"
- "What did my System.debug calls print?"
- "Run this Apex against my scratch org and analyze the performance"
- "Run it again with database logging at FINEST"
- "Find the latest debug log for integration@acme.com and summarize it"
- "Why are there no logs for this user?"

<!-- token-cost-summary:start -->

Keeping the server connected costs ~2,445 tokens, 1.2% of a 200K context. See [Token Cost](#token-cost).

<!-- token-cost-summary:end -->

## Tools Reference

The org tools use the default org of the project in your client's first [root](https://modelcontextprotocol.io/specification/latest/client/roots), unless you pass `targetOrg`. A client that declares no roots gets the default org of the server's working directory, or the global default org. When the server cannot read the roots a client declares, each org tool needs `targetOrg`, since the project's default org cannot be found. That happens when the client uses MCP protocol version 2026-07-28, when the roots cannot be read - for example, the client does not answer in 5 seconds - or when no root is on this machine.

Each tool returns its result in [TOON](https://github.com/toon-format/toon), and an error as plain text.

A `0` means none, not "not measured". Only what did not happen is left out: fatal errors, lost log content, query plans. Durations are in milliseconds to 3 decimal places, percentages to 1.

An analysis tool that pages gives `matchedCount`, the rows that matched in all, and, while more remain, `nextOffset`, the `offset` of the next page.

The server runs as a local process started by your client over stdio, with no API keys. The analysis tools make no network calls. The server keeps the last parsed log for 5 minutes after its last use, so follow-up questions on that log are faster.

### apexlog_list_slow_operations

Ranks what a log spent its time on by self time - code units, methods, queries, searches, DML, flows and workflows in one table.

A default response returns:

<!-- shape-apexlog_list_slow_operations:start -->

- `capturedAt` - `{debugCategory, level}`
- `operations` - `{debugCategory, type, name, namespace, callCount, durationTotalMs, durationSelfMs, durationSelfMaxMs, selfPercentage, soqlCount, dmlCount, soslCount, rowCount, thrownCount}`
- `queryPlans` - `{operationRow, leadingOperationType, relativeCost, cardinality, sObjectCardinality}`

<!-- shape-apexlog_list_slow_operations:end -->

Each response also gives `durationTotalMs` for the whole transaction, and `returnedSelfPercentage` for the share these rows account for.

`durationSelfMaxMs` is the slowest single call in a grouped row. Read it against `durationSelfMs` to tell one bad call from many small ones. It is absent when the row is already one call.

Two columns say what a row is, both taken straight from the log:

- `debugCategory` - what Salesforce stamped on the event. It decided whether the event was logged at all, and it is the spelling `apexlog_execute_anonymous` takes.
- `type` - the event type. The category cannot imply it: `SOQL_EXECUTE_BEGIN`, `SOSL_EXECUTE_BEGIN` and `DML_BEGIN` all sit under `database`.

Watch for `ENTERING_MANAGED_PKG`. It is time a package spent where the log shows nothing, and it is often most of a transaction.

`sortBy: "heapSelfNetBytes"` ranks by retained heap instead of time, adding that column and `returnedHeapPercentage`. Both are absent otherwise. The figure is signed, so a row that released more than it took reads below zero. Heap is logged only at `apexCode` FINER and above, so check the `apexCode` row of `capturedAt` before trusting a zero.

`capturedAt` gives the level each category in the returned rows was logged at.

`queryPlans` is what the query optimizer decided about the queries behind those rows. A `relativeCost` above 1 means it will not treat the query as selective. Plans are absent when the log explained none, because explain lines need `database` FINEST.

`operationRow` points at a row of `operations`, counting from 1. Under a `namespace`, `callerNamespace` or `debugCategory` grouping a row is not one query, so the plan carries the query text in `name` instead.

<!-- params-apexlog_list_slow_operations:start -->

| Parameter       | Type | Required | Description |
| --------------- | --- | -------- | --- |
| `logFilePath`   | string | Yes      | Absolute path |
| `debugCategory` | (`apexCode` \| `apexProfiling` \| `callout` \| `dataAccess` \| `database` \| `nba` \| `system` \| `validation` \| `visualforce` \| `wave` \| `workflow`)[] | No       | Rank only these debug log categories |
| `type`          | string[] | No       | Rank only these log event types, e.g. SOQL_EXECUTE_BEGIN, DML_BEGIN, METHOD_ENTRY |
| `namespace`     | string[] | No       | Rank only these namespaces |
| `minSelfMs`     | number | No       | Drop operations below this self time (default: 0), whichever sortBy is used |
| `limit`         | number ≥ 0 | No       | Page size (default: 10); fewer if the page would be too large |
| `offset`        | number ≥ 0 | No       | Ranked rows to skip (default: 0): the last page's nextOffset |
| `groupBy`       | `name` \| `namespace` \| `callerNamespace` \| `debugCategory` \| `none` | No       | Fold repeats into one row; default name. callerNamespace attributes platform DML to the package that drove it. debugCategory folds a namespace's event types together and so states no type or name. none ranks each call on its own. A grouped durationTotalMs is what the transaction takes back if the group never runs - never sum it across rows. |
| `sortBy`        | `durationSelfMs` \| `heapSelfNetBytes` | No       | Default durationSelfMs. heapSelfNetBytes adds that column. |

<!-- params-apexlog_list_slow_operations:end -->

### apexlog_get_summary

How long the transaction ran, where the time went, what it consumed, and whether the log is complete. Start here.

<!-- shape-apexlog_get_summary:start -->

- `fatalErrors` - `{message, frames}`
- `governorLimits` - `{limit, used, max}`
- `limitsByNamespace` - `{namespace, limit, used}`
- `categories` - `{debugCategory, level, operationCount, durationSelfMs, selfPercentage}`

<!-- shape-apexlog_get_summary:end -->

All thirteen governor limits are listed.

`limitsByNamespace` shows what each namespace consumed. This is how you see a managed package spending your CPU time. It has no ceiling column, because a ceiling is per limit for the whole transaction and already sits in `governorLimits`.

`categories` covers all eleven, each with the level it was captured at, because the level is what a zero means:

- `database,NONE,0` - the queries were not logged.
- `database,FINEST,0` - no queries ran.
- `dataAccess,"",0` - the log's header declared no level for it, which most logs do not.

`dataAccess`, `wave` and `validation` are always zero. No timed event carries them.

A last `unattributed` row holds the time no event spans, such as the time before the transaction starts. With it, the rows add up to the whole log.

`truncated` says whether the log is complete. In a partial log, every figure is a floor. Where the platform cut it, `truncatedBy` says how - `skipped-lines` for a hole, `max-size` for a missing tail - and `skippedBytes` says how much went. Both are absent when a log merely stops mid-frame.

`thrownCount` counts the exceptions thrown.

`exceptions` appears when something was thrown: `{message, thrownIn, lineNumber, thrownCount}`, one row per message, most thrown first. The table stops at 20 rows. `exceptionGroupCount` counts the messages, so a cut table says so. The log does not say whether a throw was caught.

- `message` is the first line of the exception, cut at 200 characters. Two messages that differ only after that share a row.
- `thrownIn` and `lineNumber` say where the message was first thrown. `thrownIn` is the nearest method, constructor or code unit that the log recorded.
- Below `APEX_CODE,FINE` the log records no method. Then `thrownIn` can be a code unit while `lineNumber` is a line in a method that it called.
- A managed package states its line as `EXTERNAL`.

`flowErrorCount` counts the flow elements that failed. A flow can fail with no exception and no fatal error.

`fatalErrors` appears once per failure that ended the transaction, with the innermost three frames and a trailing `…` where there were more. It is the only field that says a transaction did not finish, because a fatal error need not breach any limit.

<!-- params-apexlog_get_summary:start -->

| Parameter     | Type   | Required | Description   |
| ------------- | ------ | -------- | ------------- |
| `logFilePath` | string | Yes      | Absolute path |

<!-- params-apexlog_get_summary:end -->

### apexlog_list_limit_risks

The governor limits nearest their ceiling, worst first.

<!-- shape-apexlog_list_limit_risks:start -->

- `capturedAt` - `{debugCategory, level}`
- `atRisk` - `{limit, used, max, usedPercentage}`

<!-- shape-apexlog_list_limit_risks:end -->

`threshold` is reported beside the rows, so an empty table means nothing reached it rather than that the answer is missing.

`capturedAt` gives the level that gated each returned limit. Every limit but heap comes from `apexProfiling`; `heapSize` comes from `apexCode`.

<!-- params-apexlog_list_limit_risks:start -->

| Parameter     | Type   | Required | Description                                    |
| ------------- | ------ | -------- | ---------------------------------------------- |
| `logFilePath` | string | Yes      | Absolute path                                  |
| `threshold`   | number | No       | Minimum usedPercentage to report (default: 80) |

<!-- params-apexlog_list_limit_risks:end -->

### apexlog_search_events

Searches a log's events in log order, for what the other tools rank or total: what the code printed (`USER_DEBUG`), the validation rules, the statements, or every event under one method. Filters combine.

<!-- shape-apexlog_search_events:start -->

- `capturedAt` - `{debugCategory, level}`
- `events` - `{eventIndex, parentEventIndex, type, namespace, lineNumber, text}`

<!-- shape-apexlog_search_events:end -->

A row says what an event is and where it sits, not what it cost: rank the time with `apexlog_list_slow_operations`. `eventIndex` names an event and `parentEventIndex` the one it sits under, so a row found here can be opened up with `parentEventIndex`. An exit line is a row only where the log lost its entry, and then it is the one record that the method ran.

Text past 400 characters is elided. Ask for one event by `eventIndex` to read up to 30,000 characters of it. `contains` searches a row's text before elision, and a long row is cut around the match. For plain text, grep the saved `.log`: it is cheaper. This tool earns its tokens on event types, nesting, and clients with no shell.

`capturedAt` comes on the first page, or with one event asked for:

- When every `type` named matched, or you asked for one event, it gives the levels of the categories the matches came from.
- Otherwise, or when those name no category the header declared, it gives every level the header declared.

No `USER_DEBUG` on a log at `apexCode` `NONE` means the log did not capture debug output, not that the code printed none.

<!-- params-apexlog_search_events:start -->

| Parameter          | Type       | Required | Description                                                  |
| ------------------ | ---------- | -------- | ------------------------------------------------------------ |
| `logFilePath`      | string     | Yes      | Absolute path                                                |
| `type`             | string[]   | No       | Only these log event types, e.g. USER_DEBUG, VALIDATION_RULE |
| `namespace`        | string[]   | No       | Only these namespaces                                        |
| `contains`         | string     | No       | Only events whose text holds this, ignoring case             |
| `eventIndex`       | number ≥ 0 | No       | Only this event, with more of its text                       |
| `parentEventIndex` | number ≥ 0 | No       | Only events below this one                                   |
| `limit`            | number ≥ 0 | No       | Page size (default: 50)                                      |
| `offset`           | number ≥ 0 | No       | Rows to skip (default: 0): the last page's nextOffset        |

<!-- params-apexlog_search_events:end -->

### apexlog_execute_anonymous

Runs anonymous Apex against an authenticated org, saves the debug log locally, and returns the path. Pass that path to any analysis tool. The Apex runs as the `targetOrg` user, with that user's permissions. Its DML commits when the run succeeds; a compile failure or an uncaught exception rolls it back.

Give exactly one of `apex` or `apexFilePath`, as `sf apex run --file` takes - for example a script under `scripts/apex/`. A file outside the roots your client declares is refused, as is anything but a regular file. When the server cannot read the roots your client declares, a file is refused: see [Tools Reference](#tools-reference). A path costs a few tokens, where inline Apex is read and then written out again. A production confirmation shows the Apex the file holds, not its path.

The response gives `filePath`, `fileSizeBytes`, `org` (the username, with its alias if set), `orgType`, `succeeded`, `exceptionMessage` when the Apex threw, `durationTotalMs`, `levelsSource`, `levelsOverridden`, `outputDirCreated`, and a `warning` when something needs your attention.

When the `targetOrg` user has no live `USER_DEBUG` or `DEVELOPER_LOG` trace flag, the run adds one and deletes it after the run. The debug level that flag uses, `Apex_Log_MCP_Debug_Level`, stays in the org for the next run.

Logs go to `.apex-log-mcp/` by default - add it to your `.gitignore`. Production orgs are gated: see [Production safety](#production-safety).

<!-- params-apexlog_execute_anonymous:start -->

| Parameter      | Type | Required | Description |
| -------------- | --- | -------- | --- |
| `apex`         | string | No       | The anonymous Apex to execute, or use apexFilePath |
| `apexFilePath` | string | No       | Absolute path to a file of anonymous Apex |
| `targetOrg`    | string | No       | Alias or username of the target Salesforce org. Uses the project default if not specified. |
| `outputDir`    | string | No       | Log directory (default: .apex-log-mcp/ in the first client root, else cwd) |
| `debugLevel`   | `default` \| `NONE` \| `ERROR` \| `WARN` \| `INFO` \| `DEBUG` \| `FINE` \| `FINER` \| `FINEST` \| object: (`apexCode` \| `apexProfiling` \| `callout` \| `database` \| `nba` \| `system` \| `validation` \| `visualforce` \| `wave` \| `workflow`) → (`NONE` \| `ERROR` \| `WARN` \| `INFO` \| `DEBUG` \| `FINE` \| `FINER` \| `FINEST`) | No       | This run's log levels. Omit for the user's trace flag levels, else the defaults; "default" forces the defaults; a bare level sets every category; an object sets the named categories over the defaults. Defaults: apexCode, apexProfiling, visualforce, workflow FINE; callout, system, validation DEBUG; database FINEST; nba, wave INFO. |

<!-- params-apexlog_execute_anonymous:end -->

An object `debugLevel` looks like this:

```json
{ "database": "FINEST", "apexCode": "FINE" }
```

Set a `USER_DEBUG` trace flag on your user - in Setup, for example - and every run you start without `debugLevel` logs at its levels; your flag is never changed. `levelsSource` in the response says where a run's levels came from: `traceFlag`, `default` or `request`. A live `DEVELOPER_LOG` trace flag on your user outranks `debugLevel` and the defaults, so the run takes its levels and `levelsSource` is `traceFlag`. `levelsOverridden` says when the log carries levels other than the ones `levelsSource` names.

### apexlog_list_org_logs

Lists the debug logs stored in an org - a slow UI action, an integration user's request, a Queueable - so an agent can find one it did not run itself. Filters, sorting and paging all run in the org, so a page costs what `limit` asks for, however many logs the org holds.

`matchedCount` is how many logs the filters match. `exceptionMessage` says why a log failed. A cursor pages past the 2,000 rows SOQL's `OFFSET` stops at; `nextCursor` is absent on the last page.

<!-- params-apexlog_list_org_logs:start -->

| Parameter          | Type                                                | Required | Description |
| ------------------ | --------------------------------------------------- | -------- | --- |
| `targetOrg`        | string                                              | No       | Alias or username of the target Salesforce org. Uses the project default if not specified. |
| `user`             | string                                              | No       | Username whose activity was logged |
| `operation`        | string                                              | No       | Part of the operation, any case, e.g. "aura" for /aura |
| `request`          | string                                              | No       | e.g. "Api" or "Application" |
| `succeeded`        | boolean                                             | No       | true: succeeded logs only; false: failed logs only |
| `startTimeFrom`    | string                                              | No       | ISO 8601 with a zone, e.g. 2026-10-09T09:00:00Z |
| `startTimeTo`      | string                                              | No       |  |
| `minFileSizeBytes` | number ≥ 0                                          | No       |  |
| `sortBy`           | `startTime` \| `durationTotalMs` \| `fileSizeBytes` | No       | Descending (default: startTime) |
| `limit`            | number 1–200                                        | No       | Page size (default: 20) |
| `cursor`           | string                                              | No       | nextCursor from the previous page |

<!-- params-apexlog_list_org_logs:end -->

### apexlog_get_org_logs

Downloads logs by `ids` or the newest `latest`, as `sf apex get log` does. Returns each saved path, for the analysis tools.

A log already saved under `outputDir` is not downloaded again, since a stored log never changes; `downloaded` says which were. A log that cannot be downloaded is a row in `failed`, with the cause, and the rest still save. If `apexlog_get_summary` reports `truncated: true` with no `truncatedBy` for a saved log, the file may have been cut off while it was written: delete it and call this tool again. It reports progress as each log saves. Cancelled, it starts no more downloads; those already running finish and are saved. If the org's login stops working, it also starts no more: those ids are rows in `failed`, and `warning` says how to log in again.

<!-- params-apexlog_get_org_logs:start -->

| Parameter   | Type                  | Required | Description |
| ----------- | --------------------- | -------- | --- |
| `targetOrg` | string                | No       | Alias or username of the target Salesforce org. Uses the project default if not specified. |
| `ids`       | string[] (1–25 items) | No       | Log ids, from apexlog_list_org_logs |
| `latest`    | number 1–25           | No       | The newest N logs, in place of ids (default: 1) |
| `outputDir` | string                | No       | Log directory (default: .apex-log-mcp/ in the first client root, else cwd) |

<!-- params-apexlog_get_org_logs:end -->

### apexlog_create_trace_flag

Starts logging a user, so an integration user's requests reach the org's logs, for `apexlog_list_org_logs` to find. A flag on a class or trigger stores no log itself: it sets the levels of that code's work in the logs a user's flag stores. A user is traced as `USER_DEBUG`; a class or trigger as `CLASS_TRACING`.

The `durationMinutes` maximum is a minute short of a day, because Salesforce refuses a flag of a full day. The debug level the flag uses stays in the org after the flag ends, for the next flag at the same levels. An object `debugLevel` takes the form shown under [apexlog_execute_anonymous](#apexlog_execute_anonymous).

Every transaction a traced user runs while the flag lives is stored, which can fill the org's 1,000 MB of log storage: when it is full, no one can set a flag until logs are deleted in Setup > Debug Logs, or expire after 24 hours. When the entity already has a flag of its type whose time overlaps the new one, the call is refused and names it, with its levels, its expiry and the `sf` command that deletes it - a flag is never changed here. Against a production org, the call asks first, naming the entity, the levels and the minutes.

<!-- params-apexlog_create_trace_flag:start -->

| Parameter         | Type | Required | Description |
| ----------------- | --- | -------- | --- |
| `targetOrg`       | string | No       | Alias or username of the target Salesforce org. Uses the project default if not specified. |
| `tracedEntity`    | string | Yes      | A username or user id, or a class or trigger name (ns.Name if namespaced) |
| `debugLevel`      | `NONE` \| `ERROR` \| `WARN` \| `INFO` \| `DEBUG` \| `FINE` \| `FINER` \| `FINEST` \| object: (`apexCode` \| `apexProfiling` \| `callout` \| `database` \| `nba` \| `system` \| `validation` \| `visualforce` \| `wave` \| `workflow`) → (`NONE` \| `ERROR` \| `WARN` \| `INFO` \| `DEBUG` \| `FINE` \| `FINER` \| `FINEST`) | No       | A level for every category, or the named ones over apexlog_execute_anonymous's defaults (default: those) |
| `durationMinutes` | number 1–1439 | No       | How long it logs (default: 30) |

<!-- params-apexlog_create_trace_flag:end -->

## Token Cost

Every request carries every tool definition, whether you call them or not. Each figure below is a whole definition: name, title, description, input schema and annotations.

<!-- token-cost-definitions:start -->

| Tool                           | Tokens                                                      |
| ------------------------------ | ----------------------------------------------------------- |
| `apexlog_list_slow_operations` | ~524                                                        |
| `apexlog_execute_anonymous`    | ~424                                                        |
| `apexlog_create_trace_flag`    | ~349                                                        |
| `apexlog_search_events`        | ~330                                                        |
| `apexlog_list_org_logs`        | ~313                                                        |
| `apexlog_get_org_logs`         | ~200                                                        |
| `apexlog_get_summary`          | ~155                                                        |
| `apexlog_list_limit_risks`     | ~150                                                        |
| **Total**                      | **~2,445** (1.2% of a 200K context), **+60% vs 1.x ~1,529** |

<!-- token-cost-definitions:end -->

No single row compares with 1.x: per tool it would compare different tools, since `apexlog_list_slow_operations` replaced one that took three selection parameters and ranked methods where this one takes eight and ranks every timed event. The total is above 1.x because of the three org tools, which reach logs and trace flags 1.x could not, and `apexlog_search_events`, which reaches the events 1.x parsed and dropped.

<!-- token-cost-v1-tools:start -->

The 4 tools that replaced 1.x's now cost ~1,253 tokens.

<!-- token-cost-v1-tools:end -->

A call itself is a tool name and a log path, so what a call costs is what it returns.

Cost barely grows with the log size. The figures below are measured against a 40 KB slice of [the Apex Log Analyzer sample log](https://github.com/certinia/debug-log-analyzer/blob/main/sample-app/debug-logs/sample-log.log).

<!-- token-cost-answers:start -->

| Tool                           | Response | 1.x  | Change |
| ------------------------------ | -------- | ---- | ------ |
| `apexlog_get_summary`          | ~342     | ~293 | +17%   |
| `apexlog_list_slow_operations` | ~400     | ~408 | -2%    |
| `apexlog_list_limit_risks`     | ~35      | ~84  | -58%   |
| `apexlog_search_events`        | ~881     | -    | -      |

<!-- token-cost-answers:end -->

## Configuration

The [Quick Start](#quick-start) config gives you every tool.

### Production safety

Every org tool reads the org's type, and a `--deny-orgs` `type:` entry can refuse any of them. Only the two tools that change an org - `apexlog_execute_anonymous` and `apexlog_create_trace_flag` - use the type to decide whether to ask first:

| Org type     | Identified by                     | Behaviour             |
| ------------ | --------------------------------- | --------------------- |
| `sandbox`    | `IsSandbox`, no trial expiry      | Runs                  |
| `scratch`    | `IsSandbox` with a trial expiry   | Runs                  |
| `trial`      | Not a sandbox, has a trial expiry | Runs                  |
| `developer`  | Developer Edition                 | Runs                  |
| `production` | Anything else                     | Confirmation required |
| `unknown`    | The org could not be queried      | Confirmation required |

For a production org, `--allow-production-orgs` skips the confirmation. Otherwise the server asks you to confirm, naming the org and what the call would do - for Apex, all of it with its size. Apex over 10,000 characters is refused rather than cut, so it needs the flag. Confirmation needs a client that supports [elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation); without one the call is refused, and the message names both ways to proceed. Each confirmation authorizes one call, and expires 10 minutes after the server asks.

### Deny lists

Org type says nothing about whose data an org holds: a customer sandbox runs with no prompt. `--deny-orgs` refuses an org outright.

```json
"args": [
  "-y",
  "@certinia/apex-log-mcp",
  "--deny-orgs",
  "prod-*-org@mycompany.com,acme--*,type:production"
]
```

An entry matches the org id, username, alias or instance URL. `*` is a glob, and the match is anchored and ignores case, so `prod-*@acme.com` denies `prod-eu@acme.com` and not `xprod-eu@acme.com`. A deny on any alias of a username holds, whichever name the agent uses. Every value comes from the local sf files, so the server refuses a named org before it connects to it.

An org id matches in its 15- or 18-char form, whatever its case. An instance URL matches by its host, so `acme--*` denies every sandbox of the `acme` My Domain.

A `type:` entry denies a type from the table above, e.g. `type:sandbox`. `type:production` also denies an org whose type cannot be read, because the server treats that org as production. The server contacts the org before it refuses on type: it connects, and it queries the org type. No Apex runs and no record is written. A `type:` entry that names no org type stops the server.

Nothing lifts a deny - not `--allow-production-orgs`, not a confirmation. The refusal names what matched.

A deny covers every org tool, since a log holds the org's data. `--deny-orgs '*'` refuses them all, and the analysis tools keep working.

Only the org id is unspoofable. An alias can be re-pointed, so treat the rest as convenience, not a security boundary.

### Server flags

| Flag                      | Description                                                                                                              |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `--allow-production-orgs` | Run against production orgs, and orgs whose type cannot be read, with no confirmation. A `--deny-orgs` entry still refuses. Only set this if production targets are intentional. |
| `--no-apex-execution`     | Refuse every Apex execution. The tool stays visible, without its parameters, so agents know it exists. Every other tool still works, `apexlog_create_trace_flag` too. |
| `--read-only`             | Refuse every tool that changes an org: `apexlog_execute_anonymous` and `apexlog_create_trace_flag`. This includes `--no-apex-execution`. Both tools stay visible, without their parameters, so agents know they exist. The analysis tools and the org log tools still work, and `apexlog_get_org_logs` still saves logs to your disk. |
| `--deny-orgs`             | Refuse these orgs. See [Deny lists](#deny-lists). |

To stop Apex running, add the flag to the [Quick Start](#quick-start) `args`:

```json
"args": ["-y", "@certinia/apex-log-mcp", "--no-apex-execution"]
```

## Documentation

- [User Guide & Docs](https://certinia.github.io/debug-log-analyzer/)
- [MCP Specification](https://modelcontextprotocol.io/)

## Contributing

See the [Contributing Guide](https://github.com/certinia/debug-log-analyzer-mcp/blob/main/CONTRIBUTING.md), [Developing](https://github.com/certinia/debug-log-analyzer-mcp/blob/main/DEVELOPING.md) to set up your environment, and the [Code of Conduct](https://github.com/certinia/debug-log-analyzer-mcp/blob/main/CODE_OF_CONDUCT.md).

<p align="center">
  <a href="https://github.com/certinia/debug-log-analyzer-mcp/graphs/contributors">
    <img src="https://contrib.rocks/image?repo=certinia/debug-log-analyzer-mcp&max=25" alt="Contributors to certinia/debug-log-analyzer-mcp" />
  </a>
</p>

## License

[BSD 3-Clause](https://opensource.org/licenses/BSD-3-Clause). Copyright &copy; Certinia Inc. All rights reserved.

TDQS

A4/5.0

Scored across 8 tools

Disambiguation4/5

The tools are mostly clearly separated by resource and action: listing versus downloading org logs are distinct, and the analysis tools each target a different view of a log. Some overlap remains between get_summary, list_slow_operations, list_limit_risks, and search_events, since all analyze the same log artifact, but their descriptions make the intended use cases reasonably clear.

Naming Consistency5/5

Every tool uses the same apexlog_ prefix followed by a consistent snake_case verb_noun pattern, such as apexlog_get_org_logs, apexlog_list_slow_operations, and apexlog_search_events. The naming is predictable throughout.

Tool Count5/5

Eight tools is well scoped for an Apex log retrieval and analysis server. Each tool covers a distinct part of the workflow: obtaining logs, executing Apex, creating trace flags, and analyzing different aspects of a log.

Completeness4/5

The surface covers the core lifecycle well: listing and downloading logs, producing logs via anonymous Apex or trace flags, and several rich analysis views. Minor gaps exist around explicit trace-flag cleanup or log deletion, but agents can work around these with the available operations.

Maintenance

ActivityActive
ResponsivenessSlow