@certinia/apex-log-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +28 -0
- package/README.md +216 -0
- package/dist/ApexLogParser.js +2788 -0
- package/dist/index.js +77 -0
- package/dist/salesforce/connection.js +15 -0
- package/dist/salesforce/debugLevels.js +125 -0
- package/dist/salesforce/traceFlags.js +43 -0
- package/dist/salesforce/users.js +13 -0
- package/dist/tools/analyzeLogPerformance.js +150 -0
- package/dist/tools/executeAnonymous.js +182 -0
- package/dist/tools/findPerformanceBottlenecks.js +165 -0
- package/dist/tools/getLogSummary.js +88 -0
- package/package.json +73 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025 Certinia Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { parse } from "../ApexLogParser.js";
|
|
7
|
+
import { extractMethods } from "./analyzeLogPerformance.js";
|
|
8
|
+
import { encode } from "@toon-format/toon";
|
|
9
|
+
export const findPerformanceBottlenecksInputSchema = {
|
|
10
|
+
logFilePath: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe("Absolute path to the Apex debug log file (.log)"),
|
|
13
|
+
analysisType: z
|
|
14
|
+
.enum(["cpu", "database", "methods", "all"])
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Type of analysis: "cpu" checks CPU time governor limit, "database" checks SOQL query/DML statement/query row limits, "methods" groups methods by namespace with duration totals, "all" runs all three (default)'),
|
|
17
|
+
};
|
|
18
|
+
export const findPerformanceBottlenecksToolConfig = {
|
|
19
|
+
title: "Find Performance Bottlenecks",
|
|
20
|
+
description: "Check whether an Apex log transaction is approaching governor limits (flags usage above 80%). Analyzes CPU time, SOQL/DML limits, query rows, and method execution patterns by namespace. Best for checking if a transaction is at risk of hitting governor limits.",
|
|
21
|
+
inputSchema: findPerformanceBottlenecksInputSchema,
|
|
22
|
+
annotations: {
|
|
23
|
+
title: "Find Performance Bottlenecks",
|
|
24
|
+
readOnlyHint: true,
|
|
25
|
+
destructiveHint: false,
|
|
26
|
+
idempotentHint: true,
|
|
27
|
+
openWorldHint: false,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
export const WARNING_THRESHOLD = 80;
|
|
31
|
+
const NS_TO_MS = 1_000_000;
|
|
32
|
+
export async function findPerformanceBottlenecks(args) {
|
|
33
|
+
const { logFilePath, analysisType = "all" } = args;
|
|
34
|
+
try {
|
|
35
|
+
await fs.access(logFilePath);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`Log file not found: ${logFilePath}`);
|
|
39
|
+
}
|
|
40
|
+
const logContent = await fs.readFile(logFilePath, "utf-8");
|
|
41
|
+
const apexLog = parse(logContent);
|
|
42
|
+
const hasCpuSection = analysisType === "cpu" || analysisType === "all";
|
|
43
|
+
const bottlenecks = {};
|
|
44
|
+
if (hasCpuSection) {
|
|
45
|
+
const cpu = analyzeCPUBottlenecks(apexLog);
|
|
46
|
+
if (Object.keys(cpu).length > 0) {
|
|
47
|
+
bottlenecks.cpuBottlenecks = cpu;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (analysisType === "database" || analysisType === "all") {
|
|
51
|
+
const db = analyzeDatabaseBottlenecks(apexLog);
|
|
52
|
+
if (Object.keys(db).length > 0) {
|
|
53
|
+
bottlenecks.databaseBottlenecks = db;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (analysisType === "methods" || analysisType === "all") {
|
|
57
|
+
const methods = analyzeMethodBottlenecks(apexLog);
|
|
58
|
+
if (Object.keys(methods).length > 0) {
|
|
59
|
+
bottlenecks.methodBottlenecks = methods;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const governorWarnings = analyzeGovernorLimits(apexLog, hasCpuSection && bottlenecks.cpuBottlenecks !== undefined);
|
|
63
|
+
if (Object.keys(governorWarnings).length > 0) {
|
|
64
|
+
bottlenecks.governorLimitWarnings = governorWarnings;
|
|
65
|
+
}
|
|
66
|
+
if (Object.keys(bottlenecks).length === 0) {
|
|
67
|
+
bottlenecks.note =
|
|
68
|
+
"No performance bottlenecks or governor limit warnings found.";
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: "text",
|
|
74
|
+
text: encode(bottlenecks),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function analyzeCPUBottlenecks(apexLog) {
|
|
80
|
+
const { used, limit } = apexLog.governorLimits.cpuTime;
|
|
81
|
+
const cpuUsagePercent = limit > 0 ? (used / limit) * 100 : 0;
|
|
82
|
+
if (cpuUsagePercent > WARNING_THRESHOLD) {
|
|
83
|
+
return {
|
|
84
|
+
cpuTimeUsed: used,
|
|
85
|
+
cpuTimeLimit: limit,
|
|
86
|
+
cpuUsagePercentage: cpuUsagePercent,
|
|
87
|
+
warning: "High CPU usage detected - consider optimizing algorithms",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
function analyzeDatabaseBottlenecks(apexLog) {
|
|
93
|
+
const governorLimits = apexLog.governorLimits;
|
|
94
|
+
const bottlenecks = {};
|
|
95
|
+
const soqlPercentage = governorLimits.soqlQueries.limit > 0
|
|
96
|
+
? (governorLimits.soqlQueries.used / governorLimits.soqlQueries.limit) *
|
|
97
|
+
100
|
|
98
|
+
: 0;
|
|
99
|
+
if (soqlPercentage > WARNING_THRESHOLD) {
|
|
100
|
+
bottlenecks.soqlQueries = {
|
|
101
|
+
used: governorLimits.soqlQueries.used,
|
|
102
|
+
limit: governorLimits.soqlQueries.limit,
|
|
103
|
+
percentage: soqlPercentage,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const dmlPercentage = governorLimits.dmlStatements.limit > 0
|
|
107
|
+
? (governorLimits.dmlStatements.used /
|
|
108
|
+
governorLimits.dmlStatements.limit) *
|
|
109
|
+
100
|
|
110
|
+
: 0;
|
|
111
|
+
if (dmlPercentage > WARNING_THRESHOLD) {
|
|
112
|
+
bottlenecks.dmlStatements = {
|
|
113
|
+
used: governorLimits.dmlStatements.used,
|
|
114
|
+
limit: governorLimits.dmlStatements.limit,
|
|
115
|
+
percentage: dmlPercentage,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const queryRowsPercentage = governorLimits.queryRows.limit > 0
|
|
119
|
+
? (governorLimits.queryRows.used / governorLimits.queryRows.limit) * 100
|
|
120
|
+
: 0;
|
|
121
|
+
if (queryRowsPercentage > WARNING_THRESHOLD) {
|
|
122
|
+
bottlenecks.queryRows = {
|
|
123
|
+
used: governorLimits.queryRows.used,
|
|
124
|
+
limit: governorLimits.queryRows.limit,
|
|
125
|
+
percentage: queryRowsPercentage,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return bottlenecks;
|
|
129
|
+
}
|
|
130
|
+
function analyzeMethodBottlenecks(apexLog) {
|
|
131
|
+
const methods = extractMethods(apexLog, 0);
|
|
132
|
+
const methodsByNamespace = methods.reduce((acc, method) => {
|
|
133
|
+
if (!acc[method.namespace]) {
|
|
134
|
+
acc[method.namespace] = [];
|
|
135
|
+
}
|
|
136
|
+
acc[method.namespace].push(method);
|
|
137
|
+
return acc;
|
|
138
|
+
}, {});
|
|
139
|
+
return {
|
|
140
|
+
totalMethods: methods.length,
|
|
141
|
+
methodsByNamespace: Object.keys(methodsByNamespace).map((ns) => ({
|
|
142
|
+
namespace: ns,
|
|
143
|
+
methodCount: methodsByNamespace[ns].length,
|
|
144
|
+
totalDuration: methodsByNamespace[ns].reduce((sum, m) => sum + m.duration, 0) / NS_TO_MS,
|
|
145
|
+
})),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function analyzeGovernorLimits(apexLog, excludeCpuTime) {
|
|
149
|
+
const limits = apexLog.governorLimits;
|
|
150
|
+
const result = {};
|
|
151
|
+
Object.entries(limits).forEach(([key, value]) => {
|
|
152
|
+
if (key === "byNamespace")
|
|
153
|
+
return;
|
|
154
|
+
if (excludeCpuTime && key === "cpuTime")
|
|
155
|
+
return;
|
|
156
|
+
if (value.limit > 0) {
|
|
157
|
+
const percentage = (value.used / value.limit) * 100;
|
|
158
|
+
if (percentage > WARNING_THRESHOLD) {
|
|
159
|
+
result[key] = value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=findPerformanceBottlenecks.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025 Certinia Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { parse } from "../ApexLogParser.js";
|
|
8
|
+
import { encode } from "@toon-format/toon";
|
|
9
|
+
export const getLogSummaryInputSchema = {
|
|
10
|
+
logFilePath: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe("Absolute path to the Apex debug log file (.log)"),
|
|
13
|
+
};
|
|
14
|
+
export const getLogSummaryToolConfig = {
|
|
15
|
+
title: "Get Apex Log Summary",
|
|
16
|
+
description: "Get a high-level summary of an Apex debug log including total execution time (in ms), method count, SOQL/DML totals, governor limits, and active namespaces. Best for a quick overview before deeper analysis.",
|
|
17
|
+
inputSchema: getLogSummaryInputSchema,
|
|
18
|
+
annotations: {
|
|
19
|
+
title: "Get Apex Log Summary",
|
|
20
|
+
readOnlyHint: true,
|
|
21
|
+
destructiveHint: false,
|
|
22
|
+
idempotentHint: true,
|
|
23
|
+
openWorldHint: false,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const NS_TO_MS = 1_000_000;
|
|
27
|
+
export async function getLogSummary(args) {
|
|
28
|
+
const { logFilePath } = args;
|
|
29
|
+
try {
|
|
30
|
+
await fs.access(logFilePath);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error(`Log file not found: ${logFilePath}`);
|
|
34
|
+
}
|
|
35
|
+
const logContent = await fs.readFile(logFilePath, "utf-8");
|
|
36
|
+
const apexLog = parse(logContent);
|
|
37
|
+
const governorLimits = {};
|
|
38
|
+
Object.entries(apexLog.governorLimits).forEach(([key, value]) => {
|
|
39
|
+
if (key !== "byNamespace" && (value.used > 0 || value.limit > 0)) {
|
|
40
|
+
governorLimits[key] = { used: value.used, limit: value.limit };
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
const logIssues = apexLog.logIssues.map((issue) => ({
|
|
44
|
+
type: issue.type,
|
|
45
|
+
summary: issue.summary,
|
|
46
|
+
}));
|
|
47
|
+
const summary = {
|
|
48
|
+
file: path.basename(logFilePath),
|
|
49
|
+
size: apexLog.size,
|
|
50
|
+
totalExecutionTime: apexLog.duration.total / NS_TO_MS,
|
|
51
|
+
totalMethods: countMethods(apexLog),
|
|
52
|
+
totalSOQLQueries: apexLog.soqlCount.total,
|
|
53
|
+
totalDMLOperations: apexLog.dmlCount.total,
|
|
54
|
+
totalSOQLRows: apexLog.soqlRowCount.total,
|
|
55
|
+
totalDMLRows: apexLog.dmlRowCount.total,
|
|
56
|
+
governorLimits,
|
|
57
|
+
namespaces: apexLog.namespaces,
|
|
58
|
+
debugLevels: apexLog.debugLevels.map((d) => ({
|
|
59
|
+
category: d.logCategory,
|
|
60
|
+
level: d.logLevel,
|
|
61
|
+
})),
|
|
62
|
+
logIssues,
|
|
63
|
+
parsingErrors: apexLog.parsingErrors.length,
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: encode(summary),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function countMethods(apexLog) {
|
|
75
|
+
let count = 0;
|
|
76
|
+
const traverse = (node) => {
|
|
77
|
+
if (node.type === "METHOD_ENTRY" ||
|
|
78
|
+
node.subCategory === "Method") {
|
|
79
|
+
count++;
|
|
80
|
+
}
|
|
81
|
+
if (node.children) {
|
|
82
|
+
node.children.forEach((child) => traverse(child));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
traverse(apexLog);
|
|
86
|
+
return count;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=getLogSummary.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@certinia/apex-log-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server that gives AI assistants tools to analyze Salesforce Apex debug logs for performance bottlenecks, slow methods, and governor limit usage.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"apex-log-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*.js",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE.txt"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"salesforce",
|
|
19
|
+
"apex",
|
|
20
|
+
"log-analysis",
|
|
21
|
+
"debugging",
|
|
22
|
+
"performance",
|
|
23
|
+
"ai",
|
|
24
|
+
"developer-tools",
|
|
25
|
+
"debug-logs"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"author": {
|
|
31
|
+
"name": "Certinia",
|
|
32
|
+
"url": "https://github.com/certinia"
|
|
33
|
+
},
|
|
34
|
+
"license": "BSD-3-Clause",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/certinia/debug-log-analyzer-mcp.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/certinia/debug-log-analyzer-mcp/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/certinia/debug-log-analyzer-mcp#readme",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
45
|
+
"@salesforce/core": "^8.26.3",
|
|
46
|
+
"@toon-format/toon": "^2.1.0",
|
|
47
|
+
"zod": "^4.3.6"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@eslint/js": "^10.0.1",
|
|
51
|
+
"@swc/core": "^1.15.18",
|
|
52
|
+
"@swc/jest": "^0.2.39",
|
|
53
|
+
"@types/jest": "^30.0.0",
|
|
54
|
+
"@types/node": "^22.19.13",
|
|
55
|
+
"eslint": "^10.0.2",
|
|
56
|
+
"eslint-plugin-headers": "^1.3.4",
|
|
57
|
+
"jest": "^30.2.0",
|
|
58
|
+
"typescript": "^5.9.3",
|
|
59
|
+
"typescript-eslint": "^8.56.1"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=20.19.0"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsc",
|
|
66
|
+
"dev": "tsc --watch",
|
|
67
|
+
"start": "node dist/index.js",
|
|
68
|
+
"test": "jest",
|
|
69
|
+
"test:watch": "jest --watch",
|
|
70
|
+
"test:coverage": "jest --coverage",
|
|
71
|
+
"lint": "eslint ./src ./tests"
|
|
72
|
+
}
|
|
73
|
+
}
|