debug-log-analyzer-mcp
CLAUDE.md36 starsChanged 17 days agoBSD-3-ClauseView on GitHub
An MCP server that analyses Salesforce Apex debug logs for performance bottlenecks, governor limit usage and optimization opportunities.
- Installs packages
In this repository3 other files it gives its agents.
- changelog-entrySkill
- lean-mcp-toolsSkill
- repo-docsSkill
The file
We split this CLAUDE.md into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 93
No tool deletes stored logs, or lists or deletes trace flags: the need is rare, Setup and sf meet it, and their definitions would be …
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
An MCP server that analyses Salesforce Apex debug logs for performance bottlenecks, governor limit usage and optimization opportunities.
Development Commands
# Install dependencies
pnpm install
# Build the TypeScript project
pnpm run build
# Development with watch mode
pnpm run dev
# Run the server standalone
pnpm startArchitecture
Core Components
- src/index.ts: the
binentry point (dist/index.js). Parses flags, callsrunStdioServer, nothing else. A bad flag exits 1 with one[apex-log-mcp]line, checked bypnpm run eval. - src/server.ts:
createApexLogServer,runStdioServerandparseServerConfig. Registers the eight tools over stdio, and takes no import side effects, so tests can import it without spawning a server. - src/tools/responseShaping.ts: the shared response helpers -
toonResult,omitEmpty,warningField,toLimitRows,toNamespaceLimitRows,limitUnitsClause,limitGatingCategory,LIMIT_GATING_CATEGORIES,roundMs,roundPercent,percentageOf,NS_TO_MS,NAME_LIMIT, and the page every analysis table stops at -fitPage,PAGE_CHAR_BUDGET,nextOffsetField- with thematchesfilter they share. - src/tools/localFile.ts:
absolutePathSchema,fileReadError,fsErrorWordsandoutsideRoots, shared by every tool that reads or writes a local file - a log or a file of Apex. - src/tools/inputSchema.ts:
toolInputSchema, which wraps every tool's parameters so a parameter the tool does not have is refused, and the parameters the org tools share, worded once:targetOrgSchema,outputDirSchemaandlevelsByCategorySchema. - src/errors.ts:
errorText, what a caught value says, andtoolError, a tool's refusal or failure as a result. - src/elide.ts:
elide,elideAroundandelideEnd, which cut over-long text without splitting a character. - src/logLevels.ts: the debug log's categories and levels -
DEBUG_CATEGORIES,TRACE_CATEGORIES,LOG_LEVELS,DEFAULT_TRACE_CONFIG- with the levels a call asks for,requestedLevels, and their wording,levelsClause; anddeclaredLevelsandcapturedAt, which read the levels a log's header declared. It needs no org, so the analysis tools import it;src/salesforce/debugLevels.tswritesDebugLevelrecords from it. - src/tools/logStore.ts:
openLogStore,writeDebugLogandsaveStoredLog- where every org tool saves a log, and never over a different one - andunwritableLogDir, which refuses a directory before a step that cannot be undone, and creates nothing. - src/tools/progress.ts:
progressReporter, the progress notifications of a long org call, sent only to a caller that asked. - src/tools/analysisTool.ts:
defineAnalysisTool, which states an analysis tool once - its name, title, description, own parameters and a purerun(apexLog, args)- and addslogFilePath, the read-only annotations, the load and the TOON encoding. Test a tool throughrunon a parsed log;tests/analysisTool.test.tstests the shell.src/tools/analysisTools.tslists every analysis tool, for the server and the tests. - src/salesforce/orgAccess.ts:
OpenOrg, which a handler gets asopen, is the one way an org tool reaches an org. It runs the roots, the local org, the identity deny, the connect, the type deny and, for a write, the production gate, for one call of one tool.bindOpenOrgmakes it, and onlysrc/server.tscallsbindOpenOrg. - src/tools/*Definition.ts: an org tool's wire definition,
{ name, config }; one module can hold two tools. A write tool's definition is a function of the refusal and carries it, soregisterOrgToolanswers a refused call before the handler loads - copy a write tool, not a read tool, for a write. A definition loads at startup and loads no Salesforce SDK; the handler loads on its first call. A new org tool touches its definition, its handler, oneregisterOrgToolcall insrc/server.ts, its suite, which wires the handler throughtests/support/wiredOrgTool.ts, the tool count and handler mocks intests/server.test.ts, and aDEFINITION_BUDGETentry inscripts/eval.mjs.pnpm run evalnames anything else it lacks, andpnpm run eval:updatewrites its README tables. - src/apexLog/: the one module that imports the parser; an ESLint rule refuses it anywhere else.
index.tsholds the parser's rules the tools share -textOf,countOf,ceilingOf,multipleLogsWarning,LOG_EVENT_ID, and the parser'sDEFAULT_NAMESPACE,DEBUG_CATEGORY,DEBUG_CATEGORY_TOKEN,ENDED_BY,KIND,LEVEL,LIMIT_METRICSandeventType- and the readers of one event fromlog.columns:typeAt,frameTypeAt,namespaceAt,textAt, andtotalAtforcountOf(...).total; andisEventOf, which narrows an event so itsdetailsread as its type's. It also re-exports the parser types the tools read. A pass over every event readslog.columns, not event objects: the parser makes a new object on each read of an event. Each column reader takes its column, so a loop reads it fromlog.columnsonce;textAtalone takes the log.source.tsholdsloadApexLog, the one way the analysis tools get a log. It caches the last parse against a stat fingerprint, shares one parse between concurrent callers, and drops it five minutes after its last use, because a parsed log holds one to two times the size of the file. @apexdevtools/apex-log-parser: the parser, as a dependency - nothing here parses a log. Every value, type and const comes from the package root. ReaddebugCategoryfor an event's category, nevercategory: that one is a UI grouping slated for deprecation. ReadisFramefor whether an event has a duration.tests/parserContract.test.tspins what the tools assume, against a real parse - no other suite would notice a parser upgrade that broke one.
Key Data Structures
ApexLog: root log structure with duration, governor limits, namespacesApexEvent: one parsed event, with its parent and childrenOperation: one timed thing the transaction did, with its timing and resource usageGovernorLimits:{ snapshots, final, peak, byNamespace }. Every tool reportspeak- a counter falls when the frame that spent it exits, sofinalreads below the figure the platform enforced.
MCP Integration
Users add it to their MCP client by hand (.vscode/mcp.json, claude mcp add), and it is spoken to over stdio.
TypeScript Configuration
- Target: ES2022
- Module: NodeNext (module + moduleResolution)
- Strict mode plus extra strictness (noUncheckedIndexedAccess, noImplicitOverride, verbatimModuleSyntax, isolatedModules)
- Output to
dist/directory - Emits
.jsonly - no source maps or declarations
File Structure
src/
index.ts # CLI entry point (bin)
server.ts # MCP server implementation
*.ts # Helpers every layer may import, which import no layer: errors, elide, logLevels, compileGuards
apexLog/ # The one module that imports the log parser
tools/ # One module per MCP tool
salesforce/ # Org connection, org classification, debug levels, trace flags
policy/ # Org deny lists, for every org tool and for the writes; production gate for the writes
dist/ # Compiled JavaScript outputTools
apexlog_list_slow_operations: ranks every timed operation by self timeapexlog_get_summary: execution statistics and governor limit usageapexlog_list_limit_risks: the limits nearest their ceilingapexlog_search_events: a log's events in log order, filtered and paged, with their text and place but no timingapexlog_execute_anonymous: runs Apex, saves the log, returns its pathapexlog_list_org_logs: the debug logs stored in an org, filtered, sorted and paged in SOQLapexlog_get_org_logs: downloads stored logs by id or the newest N, returns their pathsapexlog_create_trace_flag: traces a user, or sets a class's or trigger's levels, for a while; refused when the entity has a flag of its log type already
Tools 1-4 take an absolute path to a .log file. Tools 5-8 reach an org through openOrg (src/salesforce/orgAccess.ts), so --deny-orgs holds for all of them; only a write (5 and 8) meets --deny-org-writes and the production gate.
No tool deletes stored logs, or lists or deletes trace flags: the need is rare, Setup and sf meet it, and their definitions would be charged on every request - the cost is in the DEFINITION_TOTAL_CAP comment in scripts/eval.mjs.
Naming
DEVELOPING.md holds the rules - read them before you add or rename a tool, parameter or field. The ones most easily missed: a field carries its unit (durationSelfMs, fileSizeBytes), total means including children and self excluding them, and one fact keeps one name across every tool.
Response Shaping
Responses are TOON-encoded and lean, but the saving comes from shape, never from dropping a fact - conventions in DEVELOPING.md, helpers in src/tools/responseShaping.ts. Two rules bite most often: report a fixed-schema field even at zero, since an absent count reads as one never parsed, and use omitEmpty only for occurrence lists.
A decision's evidence - a measurement, a rejected alternative - goes in a doc comment on the code it governs, or in .claude/rules/ when it is Salesforce behaviour tested in an org - not in DEVELOPING.md, which holds rules only. Read those comments before you change the code. The rules most easily broken: no prose summary or recommendations; report a table even when empty, beside the parameter that selected it, since a cutoff left unstated cannot be read; a column only one sortBy populates appears under that sortBy alone.
The tool definitions follow the same rule and are charged on every turn, called or not - the budgets and the checks are in scripts/eval.mjs. An enum already lists its values, so a .describe() must not repeat them; set title at the top level only, because annotations.title is an alias and is sent twice; anything true of every tool goes in the server instructions once.
pnpm run eval (scripts/eval.mjs, wired into CI) is the gate: it drives the built server over stdio and checks answerability, duplication, token budgets, golden files, and that startup loads no Salesforce SDK - none of which the jest suite can do, because it swaps TOON for a JSON stand-in. It also generates the README's token tables, parameter tables and response shapes, so a change that moves any of them fails until the README is regenerated with it. Re-record with pnpm run build && pnpm run eval:update and read the diff.
outputSchema/structuredContent are unimplemented on cost, not on the spec: the schema is charged in tools/list every turn, and a client is free to read structuredContent instead of our text block, spending the shaping saving. Measure both before implementing - see #66.
Anonymous Apex
apexlog_execute_anonymous writes the debug log under .apex-log-mcp/, or outputDir, and returns the path beside a summary, the org alias and the detected org type. Left out, debugLevel runs at the user's active USER_DEBUG flag's levels, else the defaults; it sets them per category, or all at once. A live DEVELOPER_LOG flag outranks all of these, debugLevel too. The levels always go in the header, because with no header the returned log is empty. It never changes the user's trace flag.
It is always registered so agents can discover it; each call passes the deny list below, then is authorized in src/policy/orgExecutionPolicy.ts. A production org, or one whose type cannot be read, needs --allow-production-orgs or a per-call confirmation, decided before any DebugLevel or TraceFlag is written. --no-apex-execution refuses every call, and --read-only refuses every call to it and to apexlog_create_trace_flag, both in src/server.ts before the tool loads; the 1.x --allowed-orgs is accepted, ignored and warned about.
--deny-orgs refuses every org tool outright, and --deny-org-writes refuses only a write, both in src/policy/orgDenyList.ts. DenyList is typed by its flag, so one list cannot take the other's place. A deny beats --allow-production-orgs and beats a confirmation. openOrg in src/salesforce/orgAccess.ts holds this order for every org tool. It runs identityRefusal on what readLocalOrg reads from the local sf files, before connectOrg, because Org.create can call the org; connectOrg reuses that AuthInfo, so the org connected is the org checked. typeRefusal runs after classifyOrg and before authorizeOperation, which knows nothing of denies. At each of the two checks, --deny-orgs goes first, then --deny-org-writes for a write. Only an org id is unspoofable, so the README documents the rest as convenience. registerOrgTool in src/server.ts binds openOrg to the tool's registered name with bindOpenOrg, and gives the result to the handler as open. Lint rules keep bindOpenOrg out of src/tools/, by a static or a dynamic import. So a handler does not bind another tool's name, and does not spend that tool's confirmation.
@salesforce/core loads only for the org tools: src/server.ts reaches each handler through await import(), and the *Definition.ts modules hold the wire definitions so registration stays synchronous. Startup is 55 ms, not 290 ms; an ESLint rule, tests/salesforceCoreIsLazy.test.ts and the pnpm run eval startup check keep it there.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

