#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs";
import * as path from "path";
import { execSync } from "child_process";
// Types
interface Issue {
line: number;
endLine?: number;
category: string;
priority: "high" | "medium" | "low";
message: string;
code: string;
fix?: string;
autoFixable: boolean;
}
interface AnalysisResult {
file: string;
issues: Issue[];
summary: {
high: number;
medium: number;
low: number;
autoFixable: number;
};
}
// Pattern definitions
const COMMENT_SLOP_PATTERNS = [
/^\s*\/\/\s*(This function|This method|Returns the|Gets the|Sets the)/i,
/^\s*\/\*\*?\s*(This function|This method|Returns the|Gets the|Sets the)/i,
/^\s*#\s*(This function|This method|Returns the|Gets the|Sets the)/i,
/^\s*\/\/\s*TODO\s*$/i,
/^\s*\/\/\s*FIXME\s*$/i,
/^\s*\/\/\s*TODO[:\s]*fix\s*(this|later)?\s*$/i,
];
const VERBOSE_LOGGING_PATTERNS = [
/(console\.log|console\.debug|console\.info)\s*\(/,
/logger\.(debug|trace|info)\s*\(\s*["'`](Entering|Exiting|Starting|Beginning|Completed|Successfully|About to)/i,
/print\s*\(\s*["'`]?(Entering|Exiting|Starting|Beginning|Completed|Successfully|About to)/i,
/logger\.(debug|trace)\s*\(/,
];
const UNNECESSARY_ERROR_PATTERNS = [
/catch\s*\([^)]*\)\s*\{\s*\}/, // Empty catch
/catch\s*\{\s*\}/, // Empty catch (Swift/Kotlin style)
];
const ASYNC_MISUSE_PATTERNS = [
/async\s+function\s+\w+\s*\([^)]*\)\s*\{[^}]*\}/, // Will check for missing await
];
const DEFENSIVE_OVERKILL_PATTERNS = [
/if\s*\(\s*\w+\s*!==?\s*null\s*&&\s*\w+\s*!==?\s*undefined\s*\)/,
/typeof\s+\w+\s*===?\s*["']string["']/,
/typeof\s+\w+\s*===?\s*["']number["']/,
/Array\.isArray\s*\(\s*\w+\s*\)/,
];
function analyzeFile(filePath: string): AnalysisResult {
const content = fs.readFileSync(filePath, "utf-8");
const lines = content.split("\n");
const issues: Issue[] = [];
const ext = path.extname(filePath).toLowerCase();
const isTS = ext === ".ts" || ext === ".tsx";
lines.forEach((line, index) => {
const lineNum = index + 1;
// Check comment slop
for (const pattern of COMMENT_SLOP_PATTERNS) {
if (pattern.test(line)) {
issues.push({
line: lineNum,
category: "Comment Slop",
priority: "high",
message: "Comment restates what code does instead of why",
code: line.trim(),
autoFixable: true,
});
break;
}
}
// Check for commented-out code
if (/^\s*\/\/\s*(const|let|var|function|class|import|export|if|for|while|return)\s/.test(line)) {
issues.push({
line: lineNum,
category: "Comment Slop",
priority: "high",
message: "Commented-out code should be removed",
code: line.trim(),
autoFixable: true,
});
}
// Check verbose logging
for (const pattern of VERBOSE_LOGGING_PATTERNS) {
if (pattern.test(line)) {
// Skip if it looks like error logging
if (/error|err|exception|fail/i.test(line)) continue;
issues.push({
line: lineNum,
category: "Verbose Logging",
priority: "high",
message: "Debug/trace logging should be removed in production",
code: line.trim(),
autoFixable: true,
});
break;
}
}
// Check unnecessary error handling
for (const pattern of UNNECESSARY_ERROR_PATTERNS) {
if (pattern.test(line)) {
issues.push({
line: lineNum,
category: "Unnecessary Error Handling",
priority: "medium",
message: "Empty catch block swallows errors silently",
code: line.trim(),
autoFixable: false,
});
break;
}
}
// Check defensive overkill (only in TypeScript)
if (isTS) {
for (const pattern of DEFENSIVE_OVERKILL_PATTERNS) {
if (pattern.test(line)) {
issues.push({
line: lineNum,
category: "Defensive Coding Overkill",
priority: "medium",
message: "Redundant type check in TypeScript - trust the type system",
code: line.trim(),
autoFixable: false,
});
break;
}
}
}
});
// Check for async functions without await
const asyncFnRegex = /async\s+(?:function\s+)?(\w+)/g;
let match;
while ((match = asyncFnRegex.exec(content)) !== null) {
const fnName = match[1];
const fnStart = match.index;
// Find the function body (simplified - looks for matching braces)
let braceCount = 0;
let fnBody = "";
let started = false;
for (let i = fnStart; i < content.length; i++) {
if (content[i] === "{") {
braceCount++;
started = true;
}
if (started) fnBody += content[i];
if (content[i] === "}") {
braceCount--;
if (braceCount === 0 && started) break;
}
}
if (fnBody && !fnBody.includes("await")) {
const lineNum = content.substring(0, fnStart).split("\n").length;
issues.push({
line: lineNum,
category: "Async Misuse",
priority: "medium",
message: `Async function '${fnName}' has no await - remove async keyword`,
code: `async ${fnName}`,
autoFixable: true,
});
}
}
// Check for unused imports (basic check)
const importRegex = /import\s+(?:\{([^}]+)\}|(\w+))\s+from/g;
while ((match = importRegex.exec(content)) !== null) {
const imports = (match[1] || match[2]).split(",").map((s) => s.trim());
for (const imp of imports) {
const cleanImp = imp.split(" as ").pop()?.trim() || imp.trim();
if (!cleanImp || cleanImp === "type") continue;
// Count occurrences (excluding the import line itself)
const importLine = content.substring(0, match.index).split("\n").length;
const restOfFile = lines.slice(importLine).join("\n");
const regex = new RegExp(`\\b${cleanImp}\\b`, "g");
const occurrences = (restOfFile.match(regex) || []).length;
if (occurrences === 0) {
issues.push({
line: importLine,
category: "Unused Import",
priority: "high",
message: `'${cleanImp}' is imported but never used`,
code: `import { ${cleanImp} }`,
autoFixable: true,
});
}
}
}
const summary = {
high: issues.filter((i) => i.priority === "high").length,
medium: issues.filter((i) => i.priority === "medium").length,
low: issues.filter((i) => i.priority === "low").length,
autoFixable: issues.filter((i) => i.autoFixable).length,
};
return { file: filePath, issues, summary };
}
function formatReport(results: AnalysisResult[]): string {
let report = "# Deslop Analysis Report\n\n";
const totalSummary = {
high: 0,
medium: 0,
low: 0,
autoFixable: 0,
};
for (const result of results) {
totalSummary.high += result.summary.high;
totalSummary.medium += result.summary.medium;
totalSummary.low += result.summary.low;
totalSummary.autoFixable += result.summary.autoFixable;
}
report += "## Summary\n";
report += `- 🔴 High Priority: ${totalSummary.high} issues\n`;
report += `- 🟡 Medium Priority: ${totalSummary.medium} issues\n`;
report += `- 🔵 Low Priority: ${totalSummary.low} issues\n`;
report += `- 🔧 Auto-fixable: ${totalSummary.autoFixable} issues\n\n`;
for (const result of results) {
if (result.issues.length === 0) continue;
report += `## ${result.file}\n\n`;
const byCategory = new Map<string, Issue[]>();
for (const issue of result.issues) {
const list = byCategory.get(issue.category) || [];
list.push(issue);
byCategory.set(issue.category, list);
}
for (const [category, issues] of byCategory) {
const priority = issues[0].priority;
const emoji = priority === "high" ? "🔴" : priority === "medium" ? "🟡" : "🔵";
report += `### ${emoji} ${category} (${issues.length})\n\n`;
for (const issue of issues) {
report += `- **Line ${issue.line}**: ${issue.message}\n`;
report += ` \`${issue.code}\`\n`;
if (issue.autoFixable) report += ` *(auto-fixable)*\n`;
report += "\n";
}
}
}
if (totalSummary.autoFixable > 0) {
report += "---\n\n";
report += `Run \`deslop_fix\` to automatically fix ${totalSummary.autoFixable} issues.\n`;
}
return report;
}
function applyFixes(filePath: string): { fixed: number; content: string } {
let content = fs.readFileSync(filePath, "utf-8");
const lines = content.split("\n");
let fixed = 0;
const linesToRemove = new Set<number>();
lines.forEach((line, index) => {
// Remove comment slop
for (const pattern of COMMENT_SLOP_PATTERNS) {
if (pattern.test(line)) {
linesToRemove.add(index);
fixed++;
break;
}
}
// Remove commented-out code
if (/^\s*\/\/\s*(const|let|var|function|class|import|export|if|for|while|return)\s/.test(line)) {
linesToRemove.add(index);
fixed++;
}
// Remove verbose logging (but not error logging)
for (const pattern of VERBOSE_LOGGING_PATTERNS) {
if (pattern.test(line) && !/error|err|exception|fail/i.test(line)) {
linesToRemove.add(index);
fixed++;
break;
}
}
});
// Remove unnecessary async keywords
content = content.replace(
/async\s+(function\s+\w+\s*\([^)]*\)\s*\{(?:(?!\bawait\b)[^{}]|\{[^{}]*\})*\})/g,
(match, fnBody) => {
if (!match.includes("await")) {
fixed++;
return fnBody;
}
return match;
}
);
// Rebuild content without removed lines
const newLines = lines.filter((_, index) => !linesToRemove.has(index));
content = newLines.join("\n");
return { fixed, content };
}
function getFilesFromGlob(pattern: string, cwd: string): string[] {
const files: string[] = [];
const walkDir = (dir: string) => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (!["node_modules", ".git", "dist", "build"].includes(entry.name)) {
walkDir(fullPath);
}
} else if (entry.isFile()) {
const ext = path.extname(entry.name).toLowerCase();
if ([".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", ".rs"].includes(ext)) {
files.push(fullPath);
}
}
}
};
if (pattern.includes("*")) {
walkDir(cwd);
} else {
const fullPath = path.resolve(cwd, pattern);
if (fs.existsSync(fullPath)) {
if (fs.statSync(fullPath).isDirectory()) {
walkDir(fullPath);
} else {
files.push(fullPath);
}
}
}
return files;
}
// Create MCP server
const server = new McpServer({
name: "deslop",
version: "1.0.0",
});
// Register tools
server.tool(
"deslop_analyze",
"Analyze code files for AI-generated slop patterns (verbose comments, unnecessary logging, redundant error handling, etc.)",
{
path: z.string().describe("File or directory path to analyze (use '.' for current directory)"),
},
async ({ path: targetPath }) => {
try {
const cwd = process.cwd();
const files = getFilesFromGlob(targetPath, cwd);
if (files.length === 0) {
return {
content: [{ type: "text", text: `No code files found at: ${targetPath}` }],
};
}
const results = files.map((f) => analyzeFile(f));
const report = formatReport(results);
return {
content: [{ type: "text", text: report }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Error analyzing: ${error}` }],
isError: true,
};
}
}
);
server.tool(
"deslop_fix",
"Automatically fix auto-fixable slop patterns in code files (removes verbose comments, debug logging, etc.)",
{
path: z.string().describe("File or directory path to fix"),
dryRun: z.boolean().optional().describe("If true, show what would be fixed without making changes"),
},
async ({ path: targetPath, dryRun = false }) => {
try {
const cwd = process.cwd();
const files = getFilesFromGlob(targetPath, cwd);
if (files.length === 0) {
return {
content: [{ type: "text", text: `No code files found at: ${targetPath}` }],
};
}
let totalFixed = 0;
const fixResults: string[] = [];
for (const file of files) {
const { fixed, content } = applyFixes(file);
if (fixed > 0) {
if (!dryRun) {
fs.writeFileSync(file, content, "utf-8");
}
totalFixed += fixed;
fixResults.push(`${file}: ${fixed} issues fixed`);
}
}
const mode = dryRun ? "(dry run)" : "";
const report =
totalFixed > 0
? `# Deslop Fix Report ${mode}\n\nFixed ${totalFixed} issues:\n\n${fixResults.map((r) => `- ${r}`).join("\n")}`
: "No auto-fixable issues found.";
return {
content: [{ type: "text", text: report }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Error fixing: ${error}` }],
isError: true,
};
}
}
);
server.tool(
"deslop_diff",
"Analyze only changed files from git diff for slop patterns",
{
staged: z.boolean().optional().describe("If true, analyze staged changes only"),
},
async ({ staged = false }) => {
try {
const cmd = staged ? "git diff --staged --name-only" : "git diff --name-only";
const output = execSync(cmd, { encoding: "utf-8" });
const files = output
.trim()
.split("\n")
.filter((f) => f && fs.existsSync(f));
if (files.length === 0) {
return {
content: [{ type: "text", text: "No changed files to analyze." }],
};
}
const results = files
.filter((f) => {
const ext = path.extname(f).toLowerCase();
return [".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", ".rs"].includes(ext);
})
.map((f) => analyzeFile(f));
const report = formatReport(results);
return {
content: [{ type: "text", text: `# Deslop Diff Analysis\n\n${report}` }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Error analyzing diff: ${error}` }],
isError: true,
};
}
}
);
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);