Skip to main content
Glama
goklab

guardvibe

scan_changed_files

Scan only files modified since a specified git ref. Returns findings for changed files, ideal for PR checks and incremental CI.

Instructions

Scan only files that have changed since a given git ref (branch, commit, or HEAD~N). Ideal for PR checks, pre-push hooks, and incremental CI. Returns findings only for modified/added files.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pathNoRepository root path.
baseNoGit ref to diff against (e.g. 'main', 'HEAD~3', commit SHA)HEAD~1
formatNoOutput formatmarkdown

Implementation Reference

  • MCP tool registration and handler for 'scan_changed_files'. Takes path, base (git ref), and format params. Runs git diff --name-only --diff-filter=ACMR against the base ref, scans each changed file using analyzeCode, and returns findings in markdown or JSON format. Records scan stats.
    // Tool 24: Scan changed files only — for incremental CI/CD and PR workflows
    server.tool(
      "scan_changed_files",
      "Scan only files that have changed since a given git ref (branch, commit, or HEAD~N). Ideal for PR checks, pre-push hooks, and incremental CI. Returns findings only for modified/added files.",
      {
        path: z.string().default(".").describe("Repository root path"),
        base: z.string().default("HEAD~1").describe("Git ref to diff against (e.g. 'main', 'HEAD~3', commit SHA)"),
        format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
      },
      async ({ path: repoPath, base, format }) => {
        const { execFileSync } = await import("child_process");
        const { readFileSync, existsSync } = await import("fs");
        const { resolve, extname, basename } = await import("path");
        const { EXTENSION_MAP, CONFIG_FILE_MAP } = await import("./utils/constants.js");
    
        const root = resolve(repoPath);
        let changedFiles: string[];
        try {
          const output = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", base], { cwd: root, encoding: "utf-8" });
          changedFiles = output.trim().split("\n").filter(Boolean);
        } catch {
          return { content: [{ type: "text", text: format === "json" ? JSON.stringify({ error: "Failed to get git diff" }) : "Error: Failed to get git diff. Ensure you're in a git repository." }] };
        }
    
        if (changedFiles.length === 0) {
          const empty = format === "json"
            ? JSON.stringify({ summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0, blocked: false }, findings: [] })
            : "No changed files to scan.";
          return { content: [{ type: "text", text: empty }] };
        }
    
        const rules = getRules();
        const allFindings: Array<{ file: string; id: string; name: string; severity: string; owasp: string; line: number; match: string; fix: string; fixCode?: string }> = [];
    
        for (const relPath of changedFiles) {
          const fullPath = resolve(root, relPath);
          if (!existsSync(fullPath)) continue;
    
          const ext = extname(relPath).toLowerCase();
          let language = EXTENSION_MAP[ext];
          if (!language && basename(relPath).startsWith("Dockerfile")) language = "dockerfile";
          if (!language) language = CONFIG_FILE_MAP[basename(relPath)];
          if (!language) continue;
    
          try {
            const content = readFileSync(fullPath, "utf-8");
            const findings = analyzeCode(content, language, undefined, fullPath, root, rules);
            for (const f of findings) {
              allFindings.push({
                file: relPath, id: f.rule.id, name: f.rule.name,
                severity: f.rule.severity, owasp: f.rule.owasp,
                line: f.line, match: f.match, fix: f.rule.fix, fixCode: f.rule.fixCode,
              });
            }
          } catch { /* skip unreadable files */ }
        }
    
        // Record stats
        recordScan(root, { toolName: "scan_changed_files", filesScanned: changedFiles.length, findings: allFindings.map(f => ({ severity: f.severity, ruleId: f.id })) });
        const statsSummary = getSummaryLine(root, allFindings.length, format);
    
        if (format === "json") {
          const critical = allFindings.filter(f => f.severity === "critical").length;
          const high = allFindings.filter(f => f.severity === "high").length;
          const medium = allFindings.filter(f => f.severity === "medium").length;
          return { content: [{ type: "text", text: mergeStatsIntoOutput(JSON.stringify({
            summary: { total: allFindings.length, critical, high, medium, low: 0, blocked: critical > 0 || high > 0, changedFiles: changedFiles.length },
            findings: allFindings,
          }), statsSummary, format) }] };
        }
    
        // Markdown
        const lines = [`# GuardVibe Changed Files Report`, ``, `Base: ${base}`, `Changed files: ${changedFiles.length}`, `Issues found: ${allFindings.length}`, ``];
        if (allFindings.length === 0) {
          lines.push(`All changed files passed security checks.`);
        } else {
          const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
          allFindings.sort((a, b) => (severityOrder[a.severity] ?? 99) - (severityOrder[b.severity] ?? 99));
          for (const f of allFindings) {
            lines.push(`- [${f.severity.toUpperCase()}] **${f.name}** (${f.id}) in \`${f.file}\`:${f.line} — ${f.fix}`);
          }
        }
        return { content: [{ type: "text", text: mergeStatsIntoOutput(lines.join("\n"), statsSummary, format) }] };
      }
    );
  • Zod schema for scan_changed_files input: path (default '.'), base (default 'HEAD~1'), format (markdown or json).
      path: z.string().default(".").describe("Repository root path"),
      base: z.string().default("HEAD~1").describe("Git ref to diff against (e.g. 'main', 'HEAD~3', commit SHA)"),
      format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
    },
  • src/index.ts:660-662 (registration)
    Registration of the scan_changed_files MCP tool via server.tool() call.
    // Tool 24: Scan changed files only — for incremental CI/CD and PR workflows
    server.tool(
      "scan_changed_files",
  • Imports used by the scan_changed_files handler: analyzeCode for security analysis, recordScan/getSummaryLine for stats, and mergeStatsIntoOutput for formatting.
    import { checkCode, analyzeCode } from "./tools/check-code.js";
    
    const require = createRequire(import.meta.url);
    const pkg = require("../package.json") as { version: string };
    import { checkProject } from "./tools/check-project.js";
    import { getSecurityDocs } from "./tools/get-security-docs.js";
    import { checkDependencies } from "./tools/check-deps.js";
    import { scanDirectory } from "./tools/scan-directory.js";
    import { scanDependencies } from "./tools/scan-dependencies.js";
    import { scanSecrets } from "./tools/scan-secrets.js";
    import { scanStaged } from "./tools/scan-staged.js";
    import { complianceReport } from "./tools/compliance-report.js";
    import { exportSarif } from "./tools/export-sarif.js";
    import { checkPackageHealth } from "./tools/check-package-health.js";
    import { fixCode } from "./tools/fix-code.js";
    import { auditConfig } from "./tools/audit-config.js";
    import { generatePolicy } from "./tools/generate-policy.js";
    import { reviewPr } from "./tools/review-pr.js";
    import { scanSecretsHistory } from "./tools/scan-secrets-history.js";
    import { policyCheck } from "./tools/policy-check.js";
    import { analyzeTaint, formatTaintFindings } from "./tools/taint-analysis.js";
    import { analyzeCrossFileTaint, formatCrossFileTaintFindings } from "./tools/cross-file-taint.js";
    import { checkCommand } from "./tools/check-command.js";
    import { scanConfigChange } from "./tools/scan-config-change.js";
    import { repoSecurityPosture } from "./tools/repo-posture.js";
    import { explainRemediation } from "./tools/explain-remediation.js";
    import { discoverPlugins } from "./plugins/loader.js";
    import { builtinRules } from "./data/rules/index.js";
    import type { SecurityRule } from "./data/rules/types.js";
    import { loadConfig } from "./utils/config.js";
    import { setRules, getRules } from "./utils/rule-registry.js";
    import { recordScan, recordFix, recordSecrets, recordDependencyCVEs, recordGrade, getSummaryLine } from "./lib/stats.js";
    import { securityStats } from "./tools/security-stats.js";
    import { auditMcpConfig } from "./tools/audit-mcp-config.js";
    import { scanHostConfig } from "./tools/scan-host-config.js";
    import { doctor } from "./tools/doctor.js";
    import { formatHostFindings, redactSecrets } from "./server/types.js";
    import { verifyFix } from "./tools/verify-fix.js";
    import { fixCode as fixCodeTool, type FixSuggestion } from "./tools/fix-code.js";
    import { analyzeAuthCoverage, formatAuthCoverage } from "./tools/auth-coverage.js";
    import { buildDeepScanPrompt, parseDeepScanResult, formatDeepScanFindings, callLLM } from "./tools/deep-scan.js";
    import { runFullAudit, formatAuditResult } from "./tools/full-audit.js";
    import { generateRemediationPlan, formatRemediationPlan } from "./tools/remediation-plan.js";
    // verify-remediation logic is inline in the tool handler below
    
    // Helper: merge stats summary into JSON output instead of concatenating two JSON objects
    function mergeStatsIntoOutput(results: string, summary: string, format: string): string {
      if (format === "json" && summary) {
        try {
          const parsed = JSON.parse(results);
          const stats = JSON.parse(summary);
          return JSON.stringify({ ...parsed, _meta: stats.guardvibeStats ?? stats });
        } catch { /* fall through */ }
      }
      return results + summary;
    }
    
    const server = new McpServer({
      name: "guardvibe",
      version: pkg.version,
      description: "Security MCP for vibe coding — single source of truth for AI assistants. 390 security rules and 36 tools. Use full_audit for a comprehensive PASS/FAIL/WARN verdict with deterministic result hash, coverage %, and unified report across code, secrets, dependencies, config, taint analysis, and auth coverage. IMPORTANT: When full_audit returns FAIL/WARN, call remediation_plan to get a mandatory section-by-section fix checklist covering ALL 6 sections (not just code). After fixing, call verify_remediation to confirm all sections were addressed. Same code = same hash = same results regardless of which AI assistant runs it. Covers OWASP, Next.js, Supabase, Stripe, Clerk, Prisma, Hono, AI SDK, MCP server security, host hardening. Maps to SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, EU AI Act. Runs 100% locally with zero configuration.",
    });
    
    // Tool 1: Analyze code for security vulnerabilities
    server.tool(
      "check_code",
      "Analyze inline code for security vulnerabilities (OWASP Top 10, XSS, SQL injection, insecure patterns). Pass code as a string parameter. For scanning files on disk, use scan_file instead. Example: check_code({code: 'app.get(...)', language: 'javascript'})",
      {
        code: z.string().describe("The code snippet to analyze"),
        language: z
          .enum(["javascript", "typescript", "python", "go", "dockerfile", "html", "sql", "shell", "yaml", "terraform", "firestore"])
          .describe("Programming language of the code"),
        framework: z
          .string()
          .optional()
          .describe("Framework context (e.g. express, nextjs, fastapi, react, django)"),
        format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
      },
      async ({ code, language, framework, format }) => {
        const rules = getRules();
        const results = checkCode(code, language, framework, undefined, undefined, format, rules);
        const findings = analyzeCode(code, language, framework, undefined, undefined, rules);
        const cwd = process.cwd();
        recordScan(cwd, { toolName: "check_code", filesScanned: 1, findings: findings.map(f => ({ severity: f.rule.severity, ruleId: f.rule.id })) });
        const summary = getSummaryLine(cwd, findings.length, format);
        return {
          content: [{ type: "text", text: mergeStatsIntoOutput(results, summary, format) }],
        };
      }
    );
    
    // Tool 2: Scan entire project for security vulnerabilities
    server.tool(
      "check_project",
      "Scan multiple files for security vulnerabilities and generate a project-wide security report with a security score. Use this for comprehensive security audits.",
      {
        files: z
          .array(
            z.object({
              path: z.string().describe("Relative file path (e.g. src/app.ts)"),
              content: z.string().describe("File source code"),
            })
          )
          .describe("List of files to scan: [{path, content}]"),
        format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
      },
      async ({ files, format }) => {
        const rules = getRules();
        const results = checkProject(files, format, rules);
        let findingCount = 0;
        const cwd = process.cwd();
        try {
          const parsed = JSON.parse(results);
          findingCount = parsed?.summary?.total ?? 0;
          const grade = parsed?.summary?.grade;
          const score = parsed?.summary?.score;
          if (grade && score != null) recordGrade(cwd, grade, score);
          recordScan(cwd, { toolName: "check_project", filesScanned: files.length, findings: (parsed?.findings ?? []).map((f: any) => ({ severity: f.severity, ruleId: f.id })) });
        } catch {
          const m = /Issues found:\s*(\d+)/.exec(results);
          findingCount = m ? parseInt(m[1], 10) : 0;
          recordScan(cwd, { toolName: "check_project", filesScanned: files.length, findings: [] });
        }
        const summary = getSummaryLine(cwd, findingCount, format);
        return {
          content: [{ type: "text", text: mergeStatsIntoOutput(results, summary, format) }],
        };
      }
    );
    
    // Tool 3: Get security documentation and best practices (renumbered from Tool 2)
    server.tool(
      "get_security_docs",
      "Get security best practices and remediation guidance for a specific topic, framework, or vulnerability type. Covers OWASP Top 10, framework-specific hardening (Next.js, Supabase, Stripe), and secure coding patterns. Returns actionable guidance with code examples.",
      {
        topic: z
          .string()
          .describe(
  • Stats recording and summary generation for scan_changed_files results.
    recordScan(root, { toolName: "scan_changed_files", filesScanned: changedFiles.length, findings: allFindings.map(f => ({ severity: f.severity, ruleId: f.id })) });
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, so description carries full burden. It explains the tool returns findings only for modified/added files and uses a git diff. Lacks mention of required git repository or error conditions, but the base parameter implies git context. Very good for no annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, no wasted words. First sentence defines action and scope; second gives use cases and output. Front-loaded with key information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no annotations and no output schema, the description is fairly complete. It tells what, when, and what output to expect. Could mention that it requires a git repository and that it's a security scanner (implied by sibling tools). Minor gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. Description does not add additional meaning beyond the schema for individual parameters; it uses the same terms. The description provides context for the tool overall but not for parameter details.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool scans only files changed since a git ref, using a specific verb ('scan') and resource ('changed files'). It distinguishes from siblings like 'scan_directory' or 'scan_staged' by emphasizing incremental scanning.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly suggests ideal use cases: PR checks, pre-push hooks, incremental CI. Also states it returns only modified/added files, implying it's not for full scans. This provides clear when-to-use and implicit when-not-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/goklab/guardvibe'

If you have feedback or need assistance with the MCP directory API, please join our Discord server