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
2 changes: 1 addition & 1 deletion packages/mcp-provider-code-analyzer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"package.json"
],
"scripts": {
"build": "tsc --build tsconfig.build.json --verbose",
"build": "tsc --build tsconfig.build.json --verbose && node scripts/copy-resources.js",
"clean": "tsc --build tsconfig.build.json --clean",
"clean-all": "yarn clean && rimraf node_modules",
"lint": "eslint **/*.ts",
Expand Down
27 changes: 27 additions & 0 deletions packages/mcp-provider-code-analyzer/scripts/copy-resources.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env node
/**
* Copies resources from src/resources to dist/resources during build
* Removes existing dist/resources content first to ensure exact match
*/
import { cpSync, rmSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageRoot = join(__dirname, '..');
const srcResources = join(packageRoot, 'src', 'resources');
const distResources = join(packageRoot, 'dist', 'resources');

try {
console.log('📦 Copying resources to dist...');
// Remove existing dist/resources to ensure exact match with src/resources
// This ensures files deleted from src/resources are also removed from dist/resources
rmSync(distResources, { recursive: true, force: true });
// Copy src/resources to dist/resources
cpSync(srcResources, distResources, { recursive: true, force: true });
console.log('✅ Resources copied successfully');
} catch (error) {
console.error('❌ Error copying resources:', error);
process.exit(1);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { getErrorMessage } from "../utils.js";

export type SupportedEngine = 'pmd' | 'eslint' | 'regex';

export type CreateCustomRuleInput = {
engine: SupportedEngine;
language: string;
};

export type CreateCustomRuleOutput = {
status: string;
knowledgeBase?: KnowledgeBase;
instructionsForLlm?: string;
nextStep?: {
action: string;
then: string;
};
error?: string;
};

export type KnowledgeBase = {
nodeIndex: string[];
nodeInfo: Record<string, {
description: string;
category?: string;
attributes: Array<{ name: string; type: string; description: string }>;
note?: string;
}>;
xpathFunctions: Array<{ name: string; syntax: string; desc: string; returnType?: string; example?: string }>;
importantNotes?: Array<{ title: string; content: string }>;
};

export interface CreateCustomRuleAction {
exec(input: CreateCustomRuleInput): Promise<CreateCustomRuleOutput>;
}

export class CreateCustomRuleActionImpl implements CreateCustomRuleAction {
private readonly knowledgeBasePath: string;

constructor(knowledgeBasePath?: string) {
if (knowledgeBasePath) {
this.knowledgeBasePath = knowledgeBasePath;
} else {
// Resources are copied to dist/resources during build, maintaining src structure
const currentDir = path.dirname(fileURLToPath(import.meta.url));
this.knowledgeBasePath = path.resolve(currentDir, '..', 'resources', 'custom-rules');
}
}

async exec(input: CreateCustomRuleInput): Promise<CreateCustomRuleOutput> {
try {
if (!this.engineSupportsCustomRules(input.engine)) {
return {
status: "error",
error: `Engine '${input.engine}' does not support custom rules or is not yet implemented.`
};
}

const normalizedLanguage = input.language.toLowerCase();
const supportedLanguages = ['apex'];
if (!supportedLanguages.includes(normalizedLanguage)) {
return {
status: "error",
error: `Language '${input.language}' support is not yet added for the Create Custom Rule MCP tool. Currently supported languages: ${supportedLanguages.join(', ')}.`
};
}
const knowledgeBase = await this.buildPMDKnowledgeBase(normalizedLanguage);

return {
status: "ready_for_xpath_generation",
knowledgeBase,
instructionsForLlm: this.getInstructionsForLlm(knowledgeBase),
nextStep: {
action: "Generate XPath rule configuration using the knowledge base",
then: "Call apply_code_analyzer_custom_rule(rule_config_json, project_root)"
}
};

} catch (e: unknown) {
return {
status: "error",
error: `Failed to prepare context: ${getErrorMessage(e)}`
};
}
}

private engineSupportsCustomRules(engine: string): boolean {
const supportedEngines: SupportedEngine[] = ['pmd'];
return supportedEngines.includes(engine as SupportedEngine);
}

private async buildPMDKnowledgeBase(language: string): Promise<KnowledgeBase> {
const astReferenceFile = `${language}-ast-reference.json`;

const astReference = this.loadKnowledgeBase('pmd', astReferenceFile);
const xpathFunctionsData = this.loadKnowledgeBase('pmd', 'xpath-functions.json');

const nodeIndex = astReference.nodes.map((n: any) => n.name);
const nodeInfo: Record<string, any> = {};

for (const node of astReference.nodes) {
nodeInfo[node.name] = {
name: node.name,
description: node.description || "",
category: node.category,
attributes: node.attributes || []
};
}

// For Apex, only universal PMD functions are available (not Java-specific ones)
const xpathFunctions = [];
const universalFunctions = xpathFunctionsData.pmd_extensions?.universal?.functions || [];
for (const func of universalFunctions) {
xpathFunctions.push({
name: func.name,
syntax: func.syntax,
desc: func.description,
returnType: func.returnType,
example: func.example
});
}

const importantNotes = (language === 'apex' && astReference.important_notes)
? astReference.important_notes
: [];

return {
nodeIndex,
nodeInfo,
xpathFunctions,
importantNotes
};
}

private loadKnowledgeBase(engine: SupportedEngine, fileName: string): any {
const filePath = path.join(this.knowledgeBasePath, engine, fileName);

if (!fs.existsSync(filePath)) {
throw new Error(`Knowledge base file not found: ${filePath}`);
}

const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content);
}

private getInstructionsForLlm(knowledgeBase: KnowledgeBase): string {
return `

YOUR TASK:
Generate PMD XPath rule configuration(s) based on the user prompt and knowledge base.

OPTIMIZED KNOWLEDGE BASE STRUCTURE:
- nodeIndex: ALL available nodes (use these names)
- nodeInfo: Detailed info for frequently used nodes
- xpathFunctions: PMD-specific XPath extension functions (pmd:* namespace)
- importantNotes: Refer to knowledgeBase.importantNotes for critical notes about common pitfalls and correct attribute usage

XPATH FUNCTIONS:
- PMD uses standard W3C XPath 3.1 functions (you already know these: ends-with, starts-with, contains, matches, not, and, or, string-length, etc.)
- PMD-specific extension functions are provided in xpathFunctions (pmd:fileName, pmd:startLine, pmd:endLine, etc.)
- Use standard XPath 3.1 functions for common operations
- Use PMD extension functions (pmd:*) when you need PMD-specific capabilities

CRITICAL REQUIREMENTS:
1. Use ONLY node names from nodeIndex (e.g., UserClass, NOT ClassNode)
2. For nodeInfo: use provided attributes
3. READ AND FOLLOW knowledgeBase.importantNotes - they contain critical information about common mistakes

SEVERITY LEVELS:
1 = Critical, 2 = High, 3 = Moderate, 4 = Low, 5 = Info

OUTPUT FORMAT (valid JSON only, no markdown):
{
"xpath": "//UserClass[not(ends-with(@Image, 'Service'))]",
"rule_name": "EnforceClassNamingSuffix",
"message": "Class name must end with 'Service'",
"severity": 2,
"description": "Enforces Service suffix for all Apex class names",
}

AFTER GENERATING THE CONFIG:
Call: apply_code_analyzer_custom_rule(rule_config_json, project_root)
`;
}
}
4 changes: 3 additions & 1 deletion packages/mcp-provider-code-analyzer/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { McpProvider, McpTool, Services } from "@salesforce/mcp-provider-api";
import { CodeAnalyzerRunMcpTool } from "./tools/run_code_analyzer.js";
import { CodeAnalyzerDescribeRuleMcpTool } from "./tools/describe_code_analyzer_rule.js";
import { CodeAnalyzerListRulesMcpTool } from "./tools/list_code_analyzer_rules.js";
import { CreateCodeAnalyzerCustomRuleMcpTool } from "./tools/create_code_analyzer_custom_rule.js";
import {CodeAnalyzerConfigFactory, CodeAnalyzerConfigFactoryImpl} from "./factories/CodeAnalyzerConfigFactory.js";
import {EnginePluginsFactory, EnginePluginsFactoryImpl} from "./factories/EnginePluginsFactory.js";
import {RunAnalyzerActionImpl} from "./actions/run-analyzer.js";
Expand Down Expand Up @@ -31,7 +32,8 @@ export class CodeAnalyzerMcpProvider extends McpProvider {
configFactory,
enginePluginsFactory,
telemetryService: services.getTelemetryService()
}))
})),
new CreateCodeAnalyzerCustomRuleMcpTool()
]);
}
}
Loading
Loading