mcp-code-context 2.1.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +44 -71
  2. package/CONTRIBUTING.md +243 -0
  3. package/MEJORAS_PROPUESTAS.md +324 -0
  4. package/README.md +31 -7
  5. package/RELEASE_NOTES_v2.2.0.md +286 -0
  6. package/dist/src/index.d.ts +36 -0
  7. package/dist/src/index.js +198 -2
  8. package/dist/src/index.js.map +1 -1
  9. package/dist/src/tools/readFileLines.d.ts +26 -0
  10. package/dist/src/tools/readFileLines.js +77 -0
  11. package/dist/src/tools/readFileLines.js.map +1 -0
  12. package/dist/src/tools/searchCodePattern.d.ts +32 -0
  13. package/dist/src/tools/searchCodePattern.js +80 -0
  14. package/dist/src/tools/searchCodePattern.js.map +1 -0
  15. package/dist/src/utils/backupManager.d.ts +13 -6
  16. package/dist/src/utils/backupManager.js +73 -8
  17. package/dist/src/utils/backupManager.js.map +1 -1
  18. package/dist/src/utils/projectRoot.d.ts +10 -0
  19. package/dist/src/utils/projectRoot.js +40 -0
  20. package/dist/src/utils/projectRoot.js.map +1 -0
  21. package/dist/tests/test-backup-system.d.ts +7 -0
  22. package/dist/tests/test-backup-system.js +152 -0
  23. package/dist/tests/test-backup-system.js.map +1 -0
  24. package/dist/tests/test-new-tools.d.ts +4 -0
  25. package/dist/tests/test-new-tools.js +147 -0
  26. package/dist/tests/test-new-tools.js.map +1 -0
  27. package/llms.txt +62 -7
  28. package/package.json +2 -2
  29. package/src/index.ts +233 -2
  30. package/src/tools/readFileLines.ts +104 -0
  31. package/src/tools/searchCodePattern.ts +129 -0
  32. package/src/utils/backupManager.ts +83 -8
  33. package/src/utils/projectRoot.ts +44 -0
  34. package/tests/test-backup-system.ts +174 -0
  35. package/tests/test-new-tools.ts +165 -0
package/src/index.ts CHANGED
@@ -40,16 +40,18 @@ import {
40
40
  type InsertPosition,
41
41
  } from "./ast/writers/symbolWriter.js";
42
42
  import { generateDiff, generateMultiFileDiff } from "./utils/diffEngine.js";
43
- import { createBackup, restoreBackup, cleanBackup } from "./utils/backupManager.js";
43
+ import { createBackup, restoreBackup, cleanBackup, cleanAllBackups } from "./utils/backupManager.js";
44
44
  import { extractFallbackSymbols, findClosestSymbols } from "./utils/fuzzyMatch.js";
45
45
  import { confirmationCache } from "./utils/confirmationCache.js";
46
+ import { readFileLines } from "./tools/readFileLines.js";
47
+ import { searchCodePattern } from "./tools/searchCodePattern.js";
46
48
  import * as fs from "node:fs";
47
49
  import * as path from "node:path";
48
50
 
49
51
  // ─── Constants ──────────────────────────────────────────────────────
50
52
 
51
53
  const SERVER_NAME = "mcp-code-context";
52
- const SERVER_VERSION = "2.0.0";
54
+ const SERVER_VERSION = "2.2.0";
53
55
 
54
56
  /** Extensions we consider source code for analysis */
55
57
  const SOURCE_EXTENSIONS = new Set([
@@ -167,6 +169,113 @@ const TOOLS = [
167
169
  required: ["filePath"],
168
170
  },
169
171
  },
172
+ {
173
+ name: "clean_backups",
174
+ description:
175
+ "Removes all backup files for a project. Deletes the entire .mcp-backups directory " +
176
+ "at the project root, freeing up disk space and cleaning the working directory. " +
177
+ "Use this to clean up after completing a series of edits or when backups are no longer needed.",
178
+ inputSchema: {
179
+ type: "object" as const,
180
+ properties: {
181
+ projectRoot: {
182
+ type: "string",
183
+ description:
184
+ "Absolute path to the project root directory. The .mcp-backups folder " +
185
+ "at this location will be removed.",
186
+ },
187
+ },
188
+ required: ["projectRoot"],
189
+ },
190
+ },
191
+ {
192
+ name: "read_file_lines",
193
+ description:
194
+ "Reads specific line ranges from a file without loading the entire content. " +
195
+ "Supports reading by exact line range (startLine/endLine) or around a pattern match. " +
196
+ "More efficient than read_file_surgical when you only need a small fragment of code. " +
197
+ "Perfect for debugging, viewing specific code blocks, or extracting context around errors.",
198
+ inputSchema: {
199
+ type: "object" as const,
200
+ properties: {
201
+ filePath: {
202
+ type: "string",
203
+ description: "Absolute path to the source file to read.",
204
+ },
205
+ startLine: {
206
+ type: "number",
207
+ description: "Starting line number (1-indexed). Required if using exact range mode.",
208
+ },
209
+ endLine: {
210
+ type: "number",
211
+ description: "Ending line number (1-indexed). Required if using exact range mode.",
212
+ },
213
+ aroundPattern: {
214
+ type: "string",
215
+ description:
216
+ "Search pattern to find in the file. Returns lines around the first match. " +
217
+ "Use this mode when you don't know the exact line numbers.",
218
+ },
219
+ contextLines: {
220
+ type: "number",
221
+ description:
222
+ "Number of lines to include before and after the pattern match. " +
223
+ "Defaults to 5. Only used with aroundPattern mode.",
224
+ },
225
+ },
226
+ required: ["filePath"],
227
+ },
228
+ },
229
+ {
230
+ name: "search_code_pattern",
231
+ description:
232
+ "Searches for code patterns across multiple files in a repository. " +
233
+ "Returns matches with file paths, line numbers, and optional context lines. " +
234
+ "Respects .gitignore rules and allows filtering by file extensions. " +
235
+ "More efficient than manual grep when you need structured results with context.",
236
+ inputSchema: {
237
+ type: "object" as const,
238
+ properties: {
239
+ rootDir: {
240
+ type: "string",
241
+ description: "Absolute path to the repository root directory to search.",
242
+ },
243
+ pattern: {
244
+ type: "string",
245
+ description:
246
+ "Regular expression pattern to search for. Use proper regex escaping " +
247
+ "(e.g., 'widget\\\\.height' to match 'widget.height').",
248
+ },
249
+ fileExtensions: {
250
+ type: "array",
251
+ items: { type: "string" },
252
+ description:
253
+ "Array of file extensions to search (e.g., ['.ts', '.dart', '.py']). " +
254
+ "Defaults to common code extensions if omitted.",
255
+ },
256
+ excludeDirs: {
257
+ type: "array",
258
+ items: { type: "string" },
259
+ description:
260
+ "Array of directory names to exclude (e.g., ['node_modules', 'dist']). " +
261
+ "Defaults to common build/dependency directories.",
262
+ },
263
+ showContext: {
264
+ type: "boolean",
265
+ description: "If true, includes surrounding lines for each match. Defaults to true.",
266
+ },
267
+ contextLines: {
268
+ type: "number",
269
+ description: "Number of context lines before/after each match. Defaults to 3.",
270
+ },
271
+ maxResults: {
272
+ type: "number",
273
+ description: "Maximum number of matches to return. Defaults to 50.",
274
+ },
275
+ },
276
+ required: ["rootDir", "pattern"],
277
+ },
278
+ },
170
279
  {
171
280
  name: "analyze_impact",
172
281
  description:
@@ -434,6 +543,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
434
543
  return await handleRollbackFile(
435
544
  args as Record<string, unknown>,
436
545
  );
546
+ case "clean_backups":
547
+ return await handleCleanBackups(
548
+ args as Record<string, unknown>,
549
+ );
550
+ case "read_file_lines":
551
+ return await handleReadFileLines(
552
+ args as Record<string, unknown>,
553
+ );
554
+ case "search_code_pattern":
555
+ return await handleSearchCodePattern(
556
+ args as Record<string, unknown>,
557
+ );
437
558
  default:
438
559
  return errorResponse(`Unknown tool: "${name}"`);
439
560
  }
@@ -1526,6 +1647,116 @@ export async function handleRollbackFile(args: Record<string, unknown>) {
1526
1647
  };
1527
1648
  }
1528
1649
 
1650
+ export async function handleCleanBackups(args: Record<string, unknown>) {
1651
+ const projectRoot = args.projectRoot as string;
1652
+
1653
+ if (!projectRoot) return errorResponse("Missing required parameter: projectRoot");
1654
+
1655
+ const resolvedPath = path.resolve(projectRoot);
1656
+
1657
+ if (!fs.existsSync(resolvedPath)) {
1658
+ return errorResponse(`Project root not found: ${resolvedPath}`);
1659
+ }
1660
+
1661
+ const success = cleanAllBackups(resolvedPath);
1662
+
1663
+ if (!success) {
1664
+ return {
1665
+ content: [{
1666
+ type: "text" as const,
1667
+ text: `// ℹ️ No backups found\n// No .mcp-backups directory exists at: ${resolvedPath}`,
1668
+ }],
1669
+ };
1670
+ }
1671
+
1672
+ return {
1673
+ content: [{
1674
+ type: "text" as const,
1675
+ text: `// 🧹 Cleanup successful!\n// Removed all backups from: ${resolvedPath}/.mcp-backups\n// Your project is now clean.`,
1676
+ }],
1677
+ };
1678
+ }
1679
+
1680
+ // ─── Tool: read_file_lines ──────────────────────────────────────────
1681
+
1682
+ export async function handleReadFileLines(args: Record<string, unknown>) {
1683
+ const result = readFileLines(args as any);
1684
+
1685
+ if (!result.success) {
1686
+ return errorResponse(result.error || "Unknown error reading file lines");
1687
+ }
1688
+
1689
+ const { content, lineRange } = result;
1690
+ const header = lineRange
1691
+ ? `// Lines ${lineRange.start}-${lineRange.end}\n// File: ${args.filePath}\n\n`
1692
+ : `// File: ${args.filePath}\n\n`;
1693
+
1694
+ return {
1695
+ content: [
1696
+ {
1697
+ type: "text" as const,
1698
+ text: header + content,
1699
+ },
1700
+ ],
1701
+ };
1702
+ }
1703
+
1704
+ // ─── Tool: search_code_pattern ──────────────────────────────────────
1705
+
1706
+ export async function handleSearchCodePattern(args: Record<string, unknown>) {
1707
+ const result = searchCodePattern(args as any);
1708
+
1709
+ if (!result.success) {
1710
+ return errorResponse(result.error || "Unknown error searching code");
1711
+ }
1712
+
1713
+ const { matches, totalMatches } = result;
1714
+
1715
+ if (!matches || matches.length === 0) {
1716
+ return {
1717
+ content: [
1718
+ {
1719
+ type: "text" as const,
1720
+ text: `# Search Results\n\nNo matches found for pattern: "${args.pattern}"`,
1721
+ },
1722
+ ],
1723
+ };
1724
+ }
1725
+
1726
+ const lines: string[] = [
1727
+ `# Search Results`,
1728
+ ``,
1729
+ `**Pattern:** \`${args.pattern}\``,
1730
+ `**Root:** \`${args.rootDir}\``,
1731
+ `**Matches:** ${totalMatches} (showing ${matches.length})`,
1732
+ ``,
1733
+ ];
1734
+
1735
+ for (const match of matches) {
1736
+ lines.push(`## \`${match.file}\` (line ${match.lineNumber})`);
1737
+ lines.push(``);
1738
+
1739
+ if (match.context) {
1740
+ lines.push("```");
1741
+ lines.push(...match.context);
1742
+ lines.push("```");
1743
+ } else {
1744
+ lines.push(`\`\`\`\n${match.line}\n\`\`\``);
1745
+ }
1746
+
1747
+ lines.push(``);
1748
+ }
1749
+
1750
+ return {
1751
+ content: [
1752
+ {
1753
+ type: "text" as const,
1754
+ text: lines.join("\n"),
1755
+ },
1756
+ ],
1757
+ };
1758
+ }
1759
+
1529
1760
  function errorResponse(message: string) {
1530
1761
  return {
1531
1762
  content: [{ type: "text" as const, text: `❌ ${message}` }],
@@ -0,0 +1,104 @@
1
+ /**
2
+ * read_file_lines — Read specific line ranges from a file
3
+ *
4
+ * Efficiently reads only the requested lines without loading the entire file into memory.
5
+ * Supports reading by exact line range or around a pattern match.
6
+ */
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+
11
+ export interface ReadFileLinesArgs {
12
+ filePath: string;
13
+ startLine?: number;
14
+ endLine?: number;
15
+ aroundPattern?: string;
16
+ contextLines?: number;
17
+ }
18
+
19
+ export interface ReadFileLinesResult {
20
+ success: boolean;
21
+ content?: string;
22
+ error?: string;
23
+ lineRange?: { start: number; end: number };
24
+ }
25
+
26
+ /**
27
+ * Read specific lines from a file
28
+ */
29
+ export function readFileLines(args: ReadFileLinesArgs): ReadFileLinesResult {
30
+ const { filePath, startLine, endLine, aroundPattern, contextLines = 5 } = args;
31
+
32
+ const resolvedPath = path.resolve(filePath);
33
+
34
+ if (!fs.existsSync(resolvedPath)) {
35
+ return {
36
+ success: false,
37
+ error: `File not found: ${resolvedPath}`,
38
+ };
39
+ }
40
+
41
+ let content: string;
42
+ try {
43
+ content = fs.readFileSync(resolvedPath, "utf-8");
44
+ } catch (error: unknown) {
45
+ return {
46
+ success: false,
47
+ error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
48
+ };
49
+ }
50
+
51
+ const lines = content.split("\n");
52
+ const totalLines = lines.length;
53
+
54
+ // Mode 1: Exact line range
55
+ if (startLine !== undefined && endLine !== undefined) {
56
+ if (startLine < 1 || endLine < 1 || startLine > totalLines || endLine > totalLines) {
57
+ return {
58
+ success: false,
59
+ error: `Invalid line range: ${startLine}-${endLine}. File has ${totalLines} lines.`,
60
+ };
61
+ }
62
+
63
+ if (startLine > endLine) {
64
+ return {
65
+ success: false,
66
+ error: `startLine (${startLine}) must be <= endLine (${endLine})`,
67
+ };
68
+ }
69
+
70
+ const selectedLines = lines.slice(startLine - 1, endLine);
71
+ return {
72
+ success: true,
73
+ content: selectedLines.join("\n"),
74
+ lineRange: { start: startLine, end: endLine },
75
+ };
76
+ }
77
+
78
+ // Mode 2: Around pattern
79
+ if (aroundPattern) {
80
+ const matchIndex = lines.findIndex((line) => line.includes(aroundPattern));
81
+
82
+ if (matchIndex === -1) {
83
+ return {
84
+ success: false,
85
+ error: `Pattern "${aroundPattern}" not found in file`,
86
+ };
87
+ }
88
+
89
+ const start = Math.max(0, matchIndex - contextLines);
90
+ const end = Math.min(totalLines - 1, matchIndex + contextLines);
91
+
92
+ const selectedLines = lines.slice(start, end + 1);
93
+ return {
94
+ success: true,
95
+ content: selectedLines.join("\n"),
96
+ lineRange: { start: start + 1, end: end + 1 },
97
+ };
98
+ }
99
+
100
+ return {
101
+ success: false,
102
+ error: "Must provide either (startLine + endLine) or aroundPattern",
103
+ };
104
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * search_code_pattern — Search for code patterns across files
3
+ *
4
+ * AST-aware search that finds patterns in code while optionally excluding
5
+ * matches in strings, comments, and other non-code contexts.
6
+ */
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { IgnoreManager } from "../utils/ignoreManager.js";
11
+
12
+ export interface SearchCodePatternArgs {
13
+ rootDir: string;
14
+ pattern: string;
15
+ fileExtensions?: string[];
16
+ excludeDirs?: string[];
17
+ showContext?: boolean;
18
+ contextLines?: number;
19
+ maxResults?: number;
20
+ astAware?: boolean;
21
+ }
22
+
23
+ export interface SearchMatch {
24
+ file: string;
25
+ lineNumber: number;
26
+ line: string;
27
+ context?: string[];
28
+ }
29
+
30
+ export interface SearchCodePatternResult {
31
+ success: boolean;
32
+ matches?: SearchMatch[];
33
+ error?: string;
34
+ totalMatches?: number;
35
+ }
36
+
37
+ const DEFAULT_EXTENSIONS = [
38
+ ".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs",
39
+ ".py", ".php", ".dart", ".java", ".go", ".rs"
40
+ ];
41
+
42
+ /**
43
+ * Search for a pattern across code files
44
+ */
45
+ export function searchCodePattern(args: SearchCodePatternArgs): SearchCodePatternResult {
46
+ const {
47
+ rootDir,
48
+ pattern,
49
+ fileExtensions = DEFAULT_EXTENSIONS,
50
+ excludeDirs = ["node_modules", "dist", "build", ".git"],
51
+ showContext = true,
52
+ contextLines = 3,
53
+ maxResults = 50,
54
+ astAware = false,
55
+ } = args;
56
+
57
+ const resolvedRoot = path.resolve(rootDir);
58
+
59
+ if (!fs.existsSync(resolvedRoot)) {
60
+ return {
61
+ success: false,
62
+ error: `Directory not found: ${resolvedRoot}`,
63
+ };
64
+ }
65
+
66
+ const ignoreManager = new IgnoreManager(resolvedRoot);
67
+ const allFiles = ignoreManager.walkDirectory();
68
+
69
+ const matches: SearchMatch[] = [];
70
+ let totalMatches = 0;
71
+
72
+ const regex = new RegExp(pattern, "gi");
73
+
74
+ for (const file of allFiles) {
75
+ // Check extension
76
+ const ext = path.extname(file).toLowerCase();
77
+ if (!fileExtensions.includes(ext)) continue;
78
+
79
+ // Check excluded directories
80
+ const relativePath = path.relative(resolvedRoot, file);
81
+ if (excludeDirs.some(dir => relativePath.includes(dir))) continue;
82
+
83
+ let content: string;
84
+ try {
85
+ content = fs.readFileSync(file, "utf-8");
86
+ } catch {
87
+ continue;
88
+ }
89
+
90
+ const lines = content.split("\n");
91
+
92
+ for (let i = 0; i < lines.length; i++) {
93
+ const line = lines[i];
94
+
95
+ if (regex.test(line)) {
96
+ totalMatches++;
97
+
98
+ if (matches.length < maxResults) {
99
+ const match: SearchMatch = {
100
+ file: path.relative(resolvedRoot, file).replace(/\\/g, "/"),
101
+ lineNumber: i + 1,
102
+ line: line.trim(),
103
+ };
104
+
105
+ if (showContext) {
106
+ const start = Math.max(0, i - contextLines);
107
+ const end = Math.min(lines.length - 1, i + contextLines);
108
+ match.context = lines.slice(start, end + 1).map((l, idx) => {
109
+ const lineNum = start + idx + 1;
110
+ const marker = lineNum === i + 1 ? ">" : " ";
111
+ return `${marker} ${lineNum}: ${l}`;
112
+ });
113
+ }
114
+
115
+ matches.push(match);
116
+ }
117
+ }
118
+
119
+ // Reset regex lastIndex for global flag
120
+ regex.lastIndex = 0;
121
+ }
122
+ }
123
+
124
+ return {
125
+ success: true,
126
+ matches,
127
+ totalMatches,
128
+ };
129
+ }
@@ -2,26 +2,40 @@
2
2
  * backupManager.ts — File Backup & Restore Utility
3
3
  *
4
4
  * Provides opt-in backup functionality for surgical write operations.
5
- * Creates `.bak` files in the same directory as the source file.
5
+ * Creates centralized `.mcp-backups/` directory at project root.
6
6
  *
7
7
  * Usage:
8
8
  * - Before modifying: createBackup(filePath) → returns backup path
9
- * - On failure: restoreBackup(filePath) → restores from .bak
10
- * - On success: cleanBackup(filePath) → removes .bak
9
+ * - On failure: restoreBackup(filePath) → restores from backup
10
+ * - Cleanup: cleanAllBackups(projectRoot) → removes all backups
11
11
  */
12
12
 
13
13
  import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
+ import { findProjectRoot } from "./projectRoot.js";
15
16
 
16
17
  /**
17
18
  * Create a backup copy of a file before modifying it.
18
- * Keeps a rolling window of the last 5 backups per file in a hidden directory.
19
+ * Keeps a rolling window of the last 5 backups per file in a centralized directory.
20
+ *
21
+ * Backups are stored at: [project-root]/.mcp-backups/[relative-path]/[filename].N.backup
19
22
  */
20
23
  export function createBackup(filePath: string): string {
21
24
  if (!fs.existsSync(filePath)) return "";
22
25
 
23
- const parsed = path.parse(filePath);
24
- const backupDir = path.join(parsed.dir, ".mcp-backups");
26
+ // Find project root
27
+ const projectRoot = findProjectRoot(filePath);
28
+ if (!projectRoot) {
29
+ // Fallback to old behavior if no project root found
30
+ return createBackupLegacy(filePath);
31
+ }
32
+
33
+ // Calculate relative path from project root
34
+ const relativePath = path.relative(projectRoot, filePath);
35
+ const parsed = path.parse(relativePath);
36
+
37
+ // Create centralized backup directory
38
+ const backupDir = path.join(projectRoot, ".mcp-backups", parsed.dir);
25
39
 
26
40
  if (!fs.existsSync(backupDir)) {
27
41
  fs.mkdirSync(backupDir, { recursive: true });
@@ -45,9 +59,53 @@ export function createBackup(filePath: string): string {
45
59
  }
46
60
 
47
61
  /**
48
- * Restore a file from its `.bak` backup.
62
+ * Legacy backup function (creates .mcp-backups in same directory)
63
+ * Used as fallback when project root cannot be determined
64
+ */
65
+ function createBackupLegacy(filePath: string): string {
66
+ const parsed = path.parse(filePath);
67
+ const backupDir = path.join(parsed.dir, ".mcp-backups");
68
+
69
+ if (!fs.existsSync(backupDir)) {
70
+ fs.mkdirSync(backupDir, { recursive: true });
71
+ }
72
+
73
+ for (let i = 4; i >= 1; i--) {
74
+ const oldB = path.join(backupDir, `${parsed.base}.${i}.backup`);
75
+ const newB = path.join(backupDir, `${parsed.base}.${i + 1}.backup`);
76
+ if (fs.existsSync(oldB)) {
77
+ if (i === 4 && fs.existsSync(newB)) {
78
+ fs.unlinkSync(newB);
79
+ }
80
+ fs.renameSync(oldB, newB);
81
+ }
82
+ }
83
+
84
+ const backupPath = path.join(backupDir, `${parsed.base}.1.backup`);
85
+ fs.copyFileSync(filePath, backupPath);
86
+ return backupPath;
87
+ }
88
+
89
+ /**
90
+ * Restore a file from its backup.
49
91
  */
50
92
  export function restoreBackup(filePath: string, steps = 1): boolean {
93
+ // Try centralized backup first
94
+ const projectRoot = findProjectRoot(filePath);
95
+
96
+ if (projectRoot) {
97
+ const relativePath = path.relative(projectRoot, filePath);
98
+ const parsed = path.parse(relativePath);
99
+ const backupDir = path.join(projectRoot, ".mcp-backups", parsed.dir);
100
+ const targetBackup = path.join(backupDir, `${parsed.base}.${steps}.backup`);
101
+
102
+ if (fs.existsSync(targetBackup)) {
103
+ fs.copyFileSync(targetBackup, filePath);
104
+ return true;
105
+ }
106
+ }
107
+
108
+ // Fallback to legacy location
51
109
  const parsed = path.parse(filePath);
52
110
  const backupDir = path.join(parsed.dir, ".mcp-backups");
53
111
  const targetBackup = path.join(backupDir, `${parsed.base}.${steps}.backup`);
@@ -59,7 +117,24 @@ export function restoreBackup(filePath: string, steps = 1): boolean {
59
117
  }
60
118
 
61
119
  /**
62
- * Clean legacy backups.
120
+ * Clean all backups for a project.
121
+ * Removes the entire .mcp-backups directory at project root.
122
+ */
123
+ export function cleanAllBackups(projectRoot: string): boolean {
124
+ const backupDir = path.join(projectRoot, ".mcp-backups");
125
+
126
+ if (!fs.existsSync(backupDir)) return false;
127
+
128
+ try {
129
+ fs.rmSync(backupDir, { recursive: true, force: true });
130
+ return true;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Clean legacy backups (no-op, kept for compatibility).
63
138
  */
64
139
  export function cleanBackup(filePath: string): void {
65
140
  // No-op. We keep the rolling 5 versions instead of deleting.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * projectRoot.ts — Project Root Discovery Utility
3
+ *
4
+ * Finds the root directory of a project by looking for common markers.
5
+ */
6
+
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+
10
+ const ROOT_MARKERS = [
11
+ "package.json",
12
+ ".git",
13
+ "pyproject.toml",
14
+ "setup.py",
15
+ "Cargo.toml",
16
+ "go.mod",
17
+ "pom.xml",
18
+ "build.gradle",
19
+ "CMakeLists.txt",
20
+ ".gitignore",
21
+ "Gemfile",
22
+ "composer.json",
23
+ "pubspec.yaml",
24
+ ];
25
+
26
+ /**
27
+ * Find the project root by walking up from a file path.
28
+ * Returns undefined if no root marker is found.
29
+ */
30
+ export function findProjectRoot(filePath: string): string | undefined {
31
+ let current = path.dirname(path.resolve(filePath));
32
+ const root = path.parse(current).root;
33
+
34
+ while (current !== root) {
35
+ for (const marker of ROOT_MARKERS) {
36
+ if (fs.existsSync(path.join(current, marker))) {
37
+ return current;
38
+ }
39
+ }
40
+ current = path.dirname(current);
41
+ }
42
+
43
+ return undefined;
44
+ }