UNPKG

@advanced-communities/salesforce-mcp-server

Version:

MCP server enabling AI assistants to interact with Salesforce orgs through the Salesforce CLI

320 lines (319 loc) 11.9 kB
import { z } from "zod"; import { executeSfCommandRaw } from "../utils/sfCommand.js"; import { permissions } from "../config/permissions.js"; const runScanner = async (target, category, engine, eslintConfig, pmdConfig, tsConfig, format, outfile, severityThreshold, normalizeSeverity, projectDir, verbose, verboseViolations) => { let command = "sf scanner run"; if (target && target.length > 0) { command += ` --target "${target.join(",")}"`; } if (category && category.length > 0) { command += ` --category "${category.join(",")}"`; } if (engine && engine.length > 0) { command += ` --engine "${engine.join(",")}"`; } if (eslintConfig) { command += ` --eslintconfig "${eslintConfig}"`; } if (pmdConfig) { command += ` --pmdconfig "${pmdConfig}"`; } if (tsConfig) { command += ` --tsconfig "${tsConfig}"`; } if (format) { command += ` --format ${format}`; } if (outfile) { command += ` --outfile "${outfile}"`; } if (severityThreshold !== undefined) { command += ` --severity-threshold ${severityThreshold}`; } if (normalizeSeverity) { command += ` --normalize-severity`; } if (projectDir && projectDir.length > 0) { command += ` --projectdir "${projectDir.join(",")}"`; } if (verbose) { command += ` --verbose`; } if (verboseViolations) { command += ` --verbose-violations`; } const result = await executeSfCommandRaw(command); return result; }; const runScannerDfa = async (target, projectDir, category, format, outfile, severityThreshold, normalizeSeverity, withPilot, verbose, ruleThreadCount, ruleThreadTimeout, ruleDisableWarningViolation, sfgeJvmArgs, pathExpLimit) => { let command = "sf scanner run dfa"; if (target && target.length > 0) { command += ` --target "${target.join(",")}"`; } if (projectDir && projectDir.length > 0) { command += ` --projectdir "${projectDir.join(",")}"`; } if (category && category.length > 0) { command += ` --category "${category.join(",")}"`; } if (format) { command += ` --format ${format}`; } if (outfile) { command += ` --outfile "${outfile}"`; } if (severityThreshold !== undefined) { command += ` --severity-threshold ${severityThreshold}`; } if (normalizeSeverity) { command += ` --normalize-severity`; } if (withPilot) { command += ` --with-pilot`; } if (verbose) { command += ` --verbose`; } if (ruleThreadCount !== undefined) { command += ` --rule-thread-count ${ruleThreadCount}`; } if (ruleThreadTimeout !== undefined) { command += ` --rule-thread-timeout ${ruleThreadTimeout}`; } if (ruleDisableWarningViolation) { command += ` --rule-disable-warning-violation`; } if (sfgeJvmArgs) { command += ` --sfgejvmargs "${sfgeJvmArgs}"`; } if (pathExpLimit !== undefined) { command += ` --pathexplimit ${pathExpLimit}`; } const result = await executeSfCommandRaw(command); return result; }; export const registerScannerTools = (server) => { server.tool("scanner_run", "Scan codebase with security and quality rules. Defaults to all rules if none specified.", { input: z.object({ target: z .array(z.string()) .optional() .describe("Source location. Supports glob patterns. Default: '.'"), category: z .array(z.string()) .optional() .describe("Rule categories to run."), engine: z .array(z.enum([ "eslint", "eslint-lwc", "eslint-typescript", "pmd", "pmd-appexchange", "retire-js", "sfge", "cpd", ])) .optional() .describe("Engines to run."), eslintConfig: z .string() .optional() .describe("ESLint config file. Cannot use with tsConfig."), pmdConfig: z.string().optional().describe("PMD rule XML file."), tsConfig: z .string() .optional() .describe("TypeScript config file. Cannot use with eslintConfig."), format: z .enum([ "csv", "html", "json", "junit", "sarif", "table", "xml", ]) .optional() .describe("Output format. Default: table"), outfile: z .string() .optional() .describe("File to write output to."), severityThreshold: z .number() .min(1) .max(3) .optional() .describe("Error on violations at/above this level: 1=high, 2=moderate, 3=low. Auto-enables normalize-severity."), normalizeSeverity: z .boolean() .optional() .describe("Include normalized severity (1=high, 2=moderate, 3=low). HTML format shows normalized only."), projectDir: z .array(z.string()) .optional() .describe("Root project directories for Graph Engine context. Must be paths, not globs."), verbose: z .boolean() .optional() .describe("Enable verbose output."), verboseViolations: z .boolean() .optional() .describe("Include Retire-js vulnerability details (CVE, URLs)."), }), }, async ({ input }) => { const { target, category, engine, eslintConfig, pmdConfig, tsConfig, format, outfile, severityThreshold, normalizeSeverity, projectDir, verbose, verboseViolations, } = input; if (permissions.isReadOnly()) { return { content: [ { type: "text", text: JSON.stringify({ success: false, message: "Scanner is disabled in read-only mode", }), }, ], }; } try { const result = await runScanner(target, category, engine, eslintConfig, pmdConfig, tsConfig, format, outfile, severityThreshold, normalizeSeverity, projectDir, verbose, verboseViolations); return { content: [ { type: "text", text: result, }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ success: false, message: error.message || "Failed to run scanner", error: error, }), }, ], }; } }); server.tool("scanner_run_dfa", "Run Graph Engine for Apex data flow analysis. Detects complex security issues like SOQL/SQL injection.", { input: z.object({ target: z .array(z.string()) .optional() .describe("Source location. Supports globs or methods with #-syntax. Default: '.'"), projectDir: z .array(z.string()) .optional() .describe("Root project directories for Graph Engine context. Must be paths, not globs."), category: z .array(z.string()) .optional() .describe("Rule categories to run."), format: z .enum([ "csv", "html", "json", "junit", "sarif", "table", "xml", ]) .optional() .describe("Output format for console."), outfile: z .string() .optional() .describe("File to write output to."), severityThreshold: z .number() .min(1) .max(3) .optional() .describe("Error on violations at/above this level: 1=high, 2=moderate, 3=low. Auto-enables normalize-severity."), normalizeSeverity: z .boolean() .optional() .describe("Include normalized severity (1=high, 2=moderate, 3=low). HTML format shows normalized only."), withPilot: z .boolean() .optional() .describe("Enable pilot rules."), verbose: z .boolean() .optional() .describe("Enable verbose output."), ruleThreadCount: z .number() .optional() .describe("Concurrent DFA evaluation threads. Inherits SFGE_RULE_THREAD_COUNT if set."), ruleThreadTimeout: z .number() .optional() .describe("Entry point evaluation timeout (ms). Inherits SFGE_RULE_THREAD_TIMEOUT if set."), ruleDisableWarningViolation: z .boolean() .optional() .describe("Disable warnings (e.g., StripInaccessible READ). Inherits SFGE_RULE_DISABLE_WARNING_VIOLATION if set."), sfgeJvmArgs: z .string() .optional() .describe("JVM arguments for Graph Engine. Space-separated."), pathExpLimit: z .number() .optional() .describe("Path expansion limit. Use -1 for unlimited. Inherits SFGE_PATH_EXPANSION_LIMIT if set."), }), }, async ({ input }) => { const { target, projectDir, category, format, outfile, severityThreshold, normalizeSeverity, withPilot, verbose, ruleThreadCount, ruleThreadTimeout, ruleDisableWarningViolation, sfgeJvmArgs, pathExpLimit, } = input; if (permissions.isReadOnly()) { return { content: [ { type: "text", text: JSON.stringify({ success: false, message: "Scanner DFA is disabled in read-only mode", }), }, ], }; } try { const result = await runScannerDfa(target, projectDir, category, format, outfile, severityThreshold, normalizeSeverity, withPilot, verbose, ruleThreadCount, ruleThreadTimeout, ruleDisableWarningViolation, sfgeJvmArgs, pathExpLimit); return { content: [ { type: "text", text: result, }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ success: false, message: error.message || "Failed to run scanner DFA", error: error, }), }, ], }; } }); };