Skip to content

Commit b6e2961

Browse files
W-20396404 : Implemented new "Create Custom Rule" MCP tool for Code Analyzer (#348)
* t/codeanalyzer/W-20396404-Create-custom-rule-mcp-tool * Adding script to copy resources to dist folder * t/codeanalyzer/W-20396404-Create-custom-rule-mcp-tool * t/codeanalyzer/W-20396404-Create-custom-rule-mcp-tool
1 parent af3f34a commit b6e2961

File tree

12 files changed

+3711
-4
lines changed

12 files changed

+3711
-4
lines changed

packages/mcp-provider-code-analyzer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"package.json"
4242
],
4343
"scripts": {
44-
"build": "tsc --build tsconfig.build.json --verbose",
44+
"build": "tsc --build tsconfig.build.json --verbose && node scripts/copy-resources.js",
4545
"clean": "tsc --build tsconfig.build.json --clean",
4646
"clean-all": "yarn clean && rimraf node_modules",
4747
"lint": "eslint **/*.ts",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Copies resources from src/resources to dist/resources during build
4+
* Removes existing dist/resources content first to ensure exact match
5+
*/
6+
import { cpSync, rmSync } from 'node:fs';
7+
import { dirname, join } from 'node:path';
8+
import { fileURLToPath } from 'node:url';
9+
10+
const __filename = fileURLToPath(import.meta.url);
11+
const __dirname = dirname(__filename);
12+
const packageRoot = join(__dirname, '..');
13+
const srcResources = join(packageRoot, 'src', 'resources');
14+
const distResources = join(packageRoot, 'dist', 'resources');
15+
16+
try {
17+
console.log('📦 Copying resources to dist...');
18+
// Remove existing dist/resources to ensure exact match with src/resources
19+
// This ensures files deleted from src/resources are also removed from dist/resources
20+
rmSync(distResources, { recursive: true, force: true });
21+
// Copy src/resources to dist/resources
22+
cpSync(srcResources, distResources, { recursive: true, force: true });
23+
console.log('✅ Resources copied successfully');
24+
} catch (error) {
25+
console.error('❌ Error copying resources:', error);
26+
process.exit(1);
27+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
import { getErrorMessage } from "../utils.js";
5+
6+
export type SupportedEngine = 'pmd' | 'eslint' | 'regex';
7+
8+
export type CreateCustomRuleInput = {
9+
engine: SupportedEngine;
10+
language: string;
11+
};
12+
13+
export type CreateCustomRuleOutput = {
14+
status: string;
15+
knowledgeBase?: KnowledgeBase;
16+
instructionsForLlm?: string;
17+
nextStep?: {
18+
action: string;
19+
then: string;
20+
};
21+
error?: string;
22+
};
23+
24+
export type KnowledgeBase = {
25+
nodeIndex: string[];
26+
nodeInfo: Record<string, {
27+
description: string;
28+
category?: string;
29+
attributes: Array<{ name: string; type: string; description: string }>;
30+
note?: string;
31+
}>;
32+
xpathFunctions: Array<{ name: string; syntax: string; desc: string; returnType?: string; example?: string }>;
33+
importantNotes?: Array<{ title: string; content: string }>;
34+
};
35+
36+
export interface CreateCustomRuleAction {
37+
exec(input: CreateCustomRuleInput): Promise<CreateCustomRuleOutput>;
38+
}
39+
40+
export class CreateCustomRuleActionImpl implements CreateCustomRuleAction {
41+
private readonly knowledgeBasePath: string;
42+
43+
constructor(knowledgeBasePath?: string) {
44+
if (knowledgeBasePath) {
45+
this.knowledgeBasePath = knowledgeBasePath;
46+
} else {
47+
// Resources are copied to dist/resources during build, maintaining src structure
48+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
49+
this.knowledgeBasePath = path.resolve(currentDir, '..', 'resources', 'custom-rules');
50+
}
51+
}
52+
53+
async exec(input: CreateCustomRuleInput): Promise<CreateCustomRuleOutput> {
54+
try {
55+
if (!this.engineSupportsCustomRules(input.engine)) {
56+
return {
57+
status: "error",
58+
error: `Engine '${input.engine}' does not support custom rules or is not yet implemented.`
59+
};
60+
}
61+
62+
const normalizedLanguage = input.language.toLowerCase();
63+
const supportedLanguages = ['apex'];
64+
if (!supportedLanguages.includes(normalizedLanguage)) {
65+
return {
66+
status: "error",
67+
error: `Language '${input.language}' support is not yet added for the Create Custom Rule MCP tool. Currently supported languages: ${supportedLanguages.join(', ')}.`
68+
};
69+
}
70+
const knowledgeBase = await this.buildPMDKnowledgeBase(normalizedLanguage);
71+
72+
return {
73+
status: "ready_for_xpath_generation",
74+
knowledgeBase,
75+
instructionsForLlm: this.getInstructionsForLlm(knowledgeBase),
76+
nextStep: {
77+
action: "Generate XPath rule configuration using the knowledge base",
78+
then: "Call apply_code_analyzer_custom_rule(rule_config_json, project_root)"
79+
}
80+
};
81+
82+
} catch (e: unknown) {
83+
return {
84+
status: "error",
85+
error: `Failed to prepare context: ${getErrorMessage(e)}`
86+
};
87+
}
88+
}
89+
90+
private engineSupportsCustomRules(engine: string): boolean {
91+
const supportedEngines: SupportedEngine[] = ['pmd'];
92+
return supportedEngines.includes(engine as SupportedEngine);
93+
}
94+
95+
private async buildPMDKnowledgeBase(language: string): Promise<KnowledgeBase> {
96+
const astReferenceFile = `${language}-ast-reference.json`;
97+
98+
const astReference = this.loadKnowledgeBase('pmd', astReferenceFile);
99+
const xpathFunctionsData = this.loadKnowledgeBase('pmd', 'xpath-functions.json');
100+
101+
const nodeIndex = astReference.nodes.map((n: any) => n.name);
102+
const nodeInfo: Record<string, any> = {};
103+
104+
for (const node of astReference.nodes) {
105+
nodeInfo[node.name] = {
106+
name: node.name,
107+
description: node.description || "",
108+
category: node.category,
109+
attributes: node.attributes || []
110+
};
111+
}
112+
113+
// For Apex, only universal PMD functions are available (not Java-specific ones)
114+
const xpathFunctions = [];
115+
const universalFunctions = xpathFunctionsData.pmd_extensions?.universal?.functions || [];
116+
for (const func of universalFunctions) {
117+
xpathFunctions.push({
118+
name: func.name,
119+
syntax: func.syntax,
120+
desc: func.description,
121+
returnType: func.returnType,
122+
example: func.example
123+
});
124+
}
125+
126+
const importantNotes = (language === 'apex' && astReference.important_notes)
127+
? astReference.important_notes
128+
: [];
129+
130+
return {
131+
nodeIndex,
132+
nodeInfo,
133+
xpathFunctions,
134+
importantNotes
135+
};
136+
}
137+
138+
private loadKnowledgeBase(engine: SupportedEngine, fileName: string): any {
139+
const filePath = path.join(this.knowledgeBasePath, engine, fileName);
140+
141+
if (!fs.existsSync(filePath)) {
142+
throw new Error(`Knowledge base file not found: ${filePath}`);
143+
}
144+
145+
const content = fs.readFileSync(filePath, 'utf-8');
146+
return JSON.parse(content);
147+
}
148+
149+
private getInstructionsForLlm(knowledgeBase: KnowledgeBase): string {
150+
return `
151+
152+
YOUR TASK:
153+
Generate PMD XPath rule configuration(s) based on the user prompt and knowledge base.
154+
155+
OPTIMIZED KNOWLEDGE BASE STRUCTURE:
156+
- nodeIndex: ALL available nodes (use these names)
157+
- nodeInfo: Detailed info for frequently used nodes
158+
- xpathFunctions: PMD-specific XPath extension functions (pmd:* namespace)
159+
- importantNotes: Refer to knowledgeBase.importantNotes for critical notes about common pitfalls and correct attribute usage
160+
161+
XPATH FUNCTIONS:
162+
- PMD uses standard W3C XPath 3.1 functions (you already know these: ends-with, starts-with, contains, matches, not, and, or, string-length, etc.)
163+
- PMD-specific extension functions are provided in xpathFunctions (pmd:fileName, pmd:startLine, pmd:endLine, etc.)
164+
- Use standard XPath 3.1 functions for common operations
165+
- Use PMD extension functions (pmd:*) when you need PMD-specific capabilities
166+
167+
CRITICAL REQUIREMENTS:
168+
1. Use ONLY node names from nodeIndex (e.g., UserClass, NOT ClassNode)
169+
2. For nodeInfo: use provided attributes
170+
3. READ AND FOLLOW knowledgeBase.importantNotes - they contain critical information about common mistakes
171+
172+
SEVERITY LEVELS:
173+
1 = Critical, 2 = High, 3 = Moderate, 4 = Low, 5 = Info
174+
175+
OUTPUT FORMAT (valid JSON only, no markdown):
176+
{
177+
"xpath": "//UserClass[not(ends-with(@Image, 'Service'))]",
178+
"rule_name": "EnforceClassNamingSuffix",
179+
"message": "Class name must end with 'Service'",
180+
"severity": 2,
181+
"description": "Enforces Service suffix for all Apex class names",
182+
}
183+
184+
AFTER GENERATING THE CONFIG:
185+
Call: apply_code_analyzer_custom_rule(rule_config_json, project_root)
186+
`;
187+
}
188+
}

packages/mcp-provider-code-analyzer/src/provider.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { McpProvider, McpTool, Services } from "@salesforce/mcp-provider-api";
22
import { CodeAnalyzerRunMcpTool } from "./tools/run_code_analyzer.js";
33
import { CodeAnalyzerDescribeRuleMcpTool } from "./tools/describe_code_analyzer_rule.js";
44
import { CodeAnalyzerListRulesMcpTool } from "./tools/list_code_analyzer_rules.js";
5+
import { CreateCodeAnalyzerCustomRuleMcpTool } from "./tools/create_code_analyzer_custom_rule.js";
56
import {CodeAnalyzerConfigFactory, CodeAnalyzerConfigFactoryImpl} from "./factories/CodeAnalyzerConfigFactory.js";
67
import {EnginePluginsFactory, EnginePluginsFactoryImpl} from "./factories/EnginePluginsFactory.js";
78
import {RunAnalyzerActionImpl} from "./actions/run-analyzer.js";
@@ -31,7 +32,8 @@ export class CodeAnalyzerMcpProvider extends McpProvider {
3132
configFactory,
3233
enginePluginsFactory,
3334
telemetryService: services.getTelemetryService()
34-
}))
35+
})),
36+
new CreateCodeAnalyzerCustomRuleMcpTool()
3537
]);
3638
}
3739
}

0 commit comments

Comments
 (0)