@rog0x/mcp-file-tools 1.0.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.
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.duplicateFinder = duplicateFinder;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const crypto = __importStar(require("crypto"));
40
+ function formatSize(bytes) {
41
+ if (bytes === 0)
42
+ return "0 B";
43
+ const units = ["B", "KB", "MB", "GB", "TB"];
44
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
45
+ return (bytes / Math.pow(1024, i)).toFixed(2) + " " + units[i];
46
+ }
47
+ function collectFiles(dirPath, depth, maxDepth, files) {
48
+ if (maxDepth > 0 && depth >= maxDepth)
49
+ return;
50
+ let entries;
51
+ try {
52
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
53
+ }
54
+ catch {
55
+ return;
56
+ }
57
+ for (const entry of entries) {
58
+ const fullPath = path.join(dirPath, entry.name);
59
+ if (entry.isDirectory()) {
60
+ if (["node_modules", ".git", "dist", "__pycache__"].includes(entry.name))
61
+ continue;
62
+ collectFiles(fullPath, depth + 1, maxDepth, files);
63
+ }
64
+ else if (entry.isFile()) {
65
+ try {
66
+ const stat = fs.statSync(fullPath);
67
+ files.push({ path: fullPath, size: stat.size });
68
+ }
69
+ catch {
70
+ // Skip inaccessible files
71
+ }
72
+ }
73
+ }
74
+ }
75
+ function hashFile(filePath) {
76
+ const content = fs.readFileSync(filePath);
77
+ return crypto.createHash("md5").update(content).digest("hex");
78
+ }
79
+ async function duplicateFinder(options) {
80
+ const { dirPath, minSize = 1, maxDepth = 10 } = options;
81
+ const resolvedPath = path.resolve(dirPath);
82
+ if (!fs.existsSync(resolvedPath)) {
83
+ throw new Error(`Directory not found: ${resolvedPath}`);
84
+ }
85
+ const allFiles = [];
86
+ collectFiles(resolvedPath, 0, maxDepth, allFiles);
87
+ // Filter by minimum size
88
+ const filteredFiles = allFiles.filter((f) => f.size >= minSize);
89
+ // Group by size first (optimization: only hash files with matching sizes)
90
+ const sizeGroups = {};
91
+ for (const file of filteredFiles) {
92
+ if (!sizeGroups[file.size]) {
93
+ sizeGroups[file.size] = [];
94
+ }
95
+ sizeGroups[file.size].push(file);
96
+ }
97
+ // Hash only files that share a size
98
+ const hashGroups = {};
99
+ for (const [, group] of Object.entries(sizeGroups)) {
100
+ if (group.length < 2)
101
+ continue;
102
+ for (const file of group) {
103
+ try {
104
+ const hash = hashFile(file.path);
105
+ if (!hashGroups[hash]) {
106
+ hashGroups[hash] = [];
107
+ }
108
+ hashGroups[hash].push(file);
109
+ }
110
+ catch {
111
+ // Skip files that can't be read
112
+ }
113
+ }
114
+ }
115
+ // Build result groups (only actual duplicates)
116
+ const groups = [];
117
+ let totalDuplicateFiles = 0;
118
+ let wastedSpaceBytes = 0;
119
+ for (const [hash, files] of Object.entries(hashGroups)) {
120
+ if (files.length < 2)
121
+ continue;
122
+ const sizeBytes = files[0].size;
123
+ groups.push({
124
+ hash,
125
+ size: formatSize(sizeBytes),
126
+ sizeBytes,
127
+ files: files.map((f) => f.path),
128
+ });
129
+ totalDuplicateFiles += files.length;
130
+ wastedSpaceBytes += sizeBytes * (files.length - 1);
131
+ }
132
+ // Sort by wasted space descending
133
+ groups.sort((a, b) => b.sizeBytes * (b.files.length - 1) - a.sizeBytes * (a.files.length - 1));
134
+ return {
135
+ directory: resolvedPath,
136
+ totalFilesScanned: filteredFiles.length,
137
+ duplicateGroups: groups.length,
138
+ totalDuplicateFiles,
139
+ wastedSpace: formatSize(wastedSpaceBytes),
140
+ wastedSpaceBytes,
141
+ groups: groups.slice(0, 50),
142
+ };
143
+ }
@@ -0,0 +1,36 @@
1
+ interface FileSearchOptions {
2
+ dirPath: string;
3
+ namePattern?: string;
4
+ contentPattern?: string;
5
+ minSize?: number;
6
+ maxSize?: number;
7
+ modifiedAfter?: string;
8
+ modifiedBefore?: string;
9
+ maxResults?: number;
10
+ }
11
+ interface SearchMatch {
12
+ path: string;
13
+ size: string;
14
+ sizeBytes: number;
15
+ modified: string;
16
+ contentMatches?: {
17
+ line: number;
18
+ text: string;
19
+ }[];
20
+ }
21
+ interface FileSearchResult {
22
+ directory: string;
23
+ query: {
24
+ namePattern?: string;
25
+ contentPattern?: string;
26
+ minSize?: number;
27
+ maxSize?: number;
28
+ modifiedAfter?: string;
29
+ modifiedBefore?: string;
30
+ };
31
+ totalMatches: number;
32
+ matches: SearchMatch[];
33
+ truncated: boolean;
34
+ }
35
+ export declare function fileSearch(options: FileSearchOptions): Promise<FileSearchResult>;
36
+ export {};
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.fileSearch = fileSearch;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const glob_1 = require("glob");
40
+ function formatSize(bytes) {
41
+ if (bytes === 0)
42
+ return "0 B";
43
+ const units = ["B", "KB", "MB", "GB", "TB"];
44
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
45
+ return (bytes / Math.pow(1024, i)).toFixed(2) + " " + units[i];
46
+ }
47
+ async function fileSearch(options) {
48
+ const { dirPath, namePattern, contentPattern, minSize, maxSize, modifiedAfter, modifiedBefore, maxResults = 100, } = options;
49
+ const resolvedPath = path.resolve(dirPath);
50
+ if (!fs.existsSync(resolvedPath)) {
51
+ throw new Error(`Directory not found: ${resolvedPath}`);
52
+ }
53
+ // Find files by name pattern using glob
54
+ const globPattern = namePattern || "**/*";
55
+ const files = await (0, glob_1.glob)(globPattern, {
56
+ cwd: resolvedPath,
57
+ nodir: true,
58
+ absolute: true,
59
+ ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/__pycache__/**"],
60
+ dot: false,
61
+ });
62
+ const matches = [];
63
+ const modAfterDate = modifiedAfter ? new Date(modifiedAfter) : null;
64
+ const modBeforeDate = modifiedBefore ? new Date(modifiedBefore) : null;
65
+ let contentRegex = null;
66
+ if (contentPattern) {
67
+ try {
68
+ contentRegex = new RegExp(contentPattern, "gi");
69
+ }
70
+ catch (e) {
71
+ const msg = e instanceof Error ? e.message : String(e);
72
+ throw new Error(`Invalid content regex: ${msg}`);
73
+ }
74
+ }
75
+ for (const filePath of files) {
76
+ if (matches.length >= maxResults)
77
+ break;
78
+ let stat;
79
+ try {
80
+ stat = fs.statSync(filePath);
81
+ }
82
+ catch {
83
+ continue;
84
+ }
85
+ // Size filters
86
+ if (minSize !== undefined && stat.size < minSize)
87
+ continue;
88
+ if (maxSize !== undefined && stat.size > maxSize)
89
+ continue;
90
+ // Date filters
91
+ if (modAfterDate && stat.mtime < modAfterDate)
92
+ continue;
93
+ if (modBeforeDate && stat.mtime > modBeforeDate)
94
+ continue;
95
+ // Content search
96
+ let contentMatches;
97
+ if (contentRegex) {
98
+ // Skip binary files and very large files
99
+ if (stat.size > 10 * 1024 * 1024)
100
+ continue;
101
+ let content;
102
+ try {
103
+ content = fs.readFileSync(filePath, "utf-8");
104
+ }
105
+ catch {
106
+ continue;
107
+ }
108
+ // Quick binary check
109
+ if (content.includes("\0"))
110
+ continue;
111
+ const lines = content.split(/\r?\n/);
112
+ contentMatches = [];
113
+ for (let i = 0; i < lines.length; i++) {
114
+ contentRegex.lastIndex = 0;
115
+ if (contentRegex.test(lines[i])) {
116
+ contentMatches.push({ line: i + 1, text: lines[i].trim().substring(0, 200) });
117
+ if (contentMatches.length >= 20)
118
+ break;
119
+ }
120
+ }
121
+ if (contentMatches.length === 0)
122
+ continue;
123
+ }
124
+ matches.push({
125
+ path: filePath,
126
+ size: formatSize(stat.size),
127
+ sizeBytes: stat.size,
128
+ modified: stat.mtime.toISOString(),
129
+ ...(contentMatches ? { contentMatches } : {}),
130
+ });
131
+ }
132
+ return {
133
+ directory: resolvedPath,
134
+ query: {
135
+ ...(namePattern ? { namePattern } : {}),
136
+ ...(contentPattern ? { contentPattern } : {}),
137
+ ...(minSize !== undefined ? { minSize } : {}),
138
+ ...(maxSize !== undefined ? { maxSize } : {}),
139
+ ...(modifiedAfter ? { modifiedAfter } : {}),
140
+ ...(modifiedBefore ? { modifiedBefore } : {}),
141
+ },
142
+ totalMatches: matches.length,
143
+ matches,
144
+ truncated: matches.length >= maxResults,
145
+ };
146
+ }
@@ -0,0 +1,25 @@
1
+ interface FileStatsOptions {
2
+ dirPath: string;
3
+ maxDepth?: number;
4
+ }
5
+ interface FileInfo {
6
+ path: string;
7
+ size: number;
8
+ modified: string;
9
+ }
10
+ interface FileStatsResult {
11
+ directory: string;
12
+ totalFiles: number;
13
+ totalSize: string;
14
+ totalSizeBytes: number;
15
+ extensionCounts: Record<string, {
16
+ count: number;
17
+ totalSize: string;
18
+ totalSizeBytes: number;
19
+ }>;
20
+ largestFiles: FileInfo[];
21
+ newestFiles: FileInfo[];
22
+ oldestFiles: FileInfo[];
23
+ }
24
+ export declare function fileStats(options: FileStatsOptions): Promise<FileStatsResult>;
25
+ export {};
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.fileStats = fileStats;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ function formatSize(bytes) {
40
+ if (bytes === 0)
41
+ return "0 B";
42
+ const units = ["B", "KB", "MB", "GB", "TB"];
43
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
44
+ return (bytes / Math.pow(1024, i)).toFixed(2) + " " + units[i];
45
+ }
46
+ function collectFiles(dirPath, depth, maxDepth, files) {
47
+ if (maxDepth > 0 && depth >= maxDepth)
48
+ return;
49
+ let entries;
50
+ try {
51
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
52
+ }
53
+ catch {
54
+ return;
55
+ }
56
+ for (const entry of entries) {
57
+ const fullPath = path.join(dirPath, entry.name);
58
+ if (entry.isDirectory()) {
59
+ if (["node_modules", ".git", "dist", "__pycache__"].includes(entry.name))
60
+ continue;
61
+ collectFiles(fullPath, depth + 1, maxDepth, files);
62
+ }
63
+ else if (entry.isFile()) {
64
+ try {
65
+ const stat = fs.statSync(fullPath);
66
+ files.push({
67
+ path: fullPath,
68
+ size: stat.size,
69
+ modified: stat.mtime.toISOString(),
70
+ });
71
+ }
72
+ catch {
73
+ // Skip inaccessible files
74
+ }
75
+ }
76
+ }
77
+ }
78
+ async function fileStats(options) {
79
+ const { dirPath, maxDepth = 10 } = options;
80
+ const resolvedPath = path.resolve(dirPath);
81
+ if (!fs.existsSync(resolvedPath)) {
82
+ throw new Error(`Directory not found: ${resolvedPath}`);
83
+ }
84
+ const files = [];
85
+ collectFiles(resolvedPath, 0, maxDepth, files);
86
+ const totalSizeBytes = files.reduce((sum, f) => sum + f.size, 0);
87
+ // Count by extension
88
+ const extMap = {};
89
+ for (const file of files) {
90
+ const ext = path.extname(file.path).toLowerCase() || "(no extension)";
91
+ if (!extMap[ext]) {
92
+ extMap[ext] = { count: 0, totalSizeBytes: 0 };
93
+ }
94
+ extMap[ext].count++;
95
+ extMap[ext].totalSizeBytes += file.size;
96
+ }
97
+ const extensionCounts = {};
98
+ const sortedExts = Object.entries(extMap).sort((a, b) => b[1].count - a[1].count);
99
+ for (const [ext, data] of sortedExts) {
100
+ extensionCounts[ext] = {
101
+ count: data.count,
102
+ totalSize: formatSize(data.totalSizeBytes),
103
+ totalSizeBytes: data.totalSizeBytes,
104
+ };
105
+ }
106
+ // Largest files (top 10)
107
+ const largestFiles = [...files]
108
+ .sort((a, b) => b.size - a.size)
109
+ .slice(0, 10);
110
+ // Newest files (top 10)
111
+ const newestFiles = [...files]
112
+ .sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime())
113
+ .slice(0, 10);
114
+ // Oldest files (top 10)
115
+ const oldestFiles = [...files]
116
+ .sort((a, b) => new Date(a.modified).getTime() - new Date(b.modified).getTime())
117
+ .slice(0, 10);
118
+ return {
119
+ directory: resolvedPath,
120
+ totalFiles: files.length,
121
+ totalSize: formatSize(totalSizeBytes),
122
+ totalSizeBytes,
123
+ extensionCounts,
124
+ largestFiles,
125
+ newestFiles,
126
+ oldestFiles,
127
+ };
128
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@rog0x/mcp-file-tools",
3
+ "version": "1.0.0",
4
+ "description": "MCP server providing file and directory analysis tools for AI agents",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "mcp-file-tools": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js",
12
+ "dev": "ts-node src/index.ts"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "claude",
17
+ "ai",
18
+ "file-tools",
19
+ "directory-tree",
20
+ "code-counter",
21
+ "duplicate-finder"
22
+ ],
23
+ "author": "rog0x",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/rog0x/mcp-file-tools"
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.0.0",
31
+ "glob": "^11.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.0.0",
35
+ "typescript": "^5.4.0"
36
+ }
37
+ }