@supermodeltools/mcp-server 0.4.3 → 0.4.4

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,332 @@
1
+ "use strict";
2
+ /**
3
+ * Automatic repository zipping with gitignore support
4
+ * Creates temporary ZIP files for codebase analysis
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.zipRepository = zipRepository;
11
+ exports.cleanupOldZips = cleanupOldZips;
12
+ const fs_1 = require("fs");
13
+ const path_1 = require("path");
14
+ const os_1 = require("os");
15
+ const archiver_1 = __importDefault(require("archiver"));
16
+ const ignore_1 = __importDefault(require("ignore"));
17
+ const crypto_1 = require("crypto");
18
+ /**
19
+ * Standard exclusions for security and size optimization
20
+ * These patterns are applied in addition to .gitignore
21
+ */
22
+ const STANDARD_EXCLUSIONS = [
23
+ // Version control
24
+ '.git',
25
+ '.svn',
26
+ '.hg',
27
+ // Dependencies
28
+ 'node_modules',
29
+ 'vendor',
30
+ 'venv',
31
+ '.venv',
32
+ 'env',
33
+ 'virtualenv',
34
+ 'target', // Rust/Java
35
+ // Build outputs
36
+ 'dist',
37
+ 'build',
38
+ 'out',
39
+ '.next',
40
+ '__pycache__',
41
+ '*.pyc',
42
+ '*.pyo',
43
+ '*.so',
44
+ '*.dylib',
45
+ '*.dll',
46
+ '*.class',
47
+ // IDE files
48
+ '.idea',
49
+ '.vscode',
50
+ '.vs',
51
+ '*.swp',
52
+ '*.swo',
53
+ '*~',
54
+ '.DS_Store',
55
+ // Sensitive files (CRITICAL - prevent credential leaks)
56
+ '.env',
57
+ '.env.local',
58
+ '.env.*.local',
59
+ '*.pem',
60
+ '*.key',
61
+ '*.p12',
62
+ '*.pfx',
63
+ '*.jks',
64
+ 'secrets.yml',
65
+ 'secrets.yaml',
66
+ 'secrets.json',
67
+ 'credentials.json',
68
+ 'serviceaccount.json',
69
+ '.aws/credentials',
70
+ '.ssh/id_rsa',
71
+ '.ssh/id_ed25519',
72
+ // Large binary files
73
+ '*.mp4',
74
+ '*.avi',
75
+ '*.mov',
76
+ '*.zip',
77
+ '*.tar',
78
+ '*.gz',
79
+ '*.rar',
80
+ '*.7z',
81
+ '*.iso',
82
+ '*.dmg',
83
+ ];
84
+ /**
85
+ * Create a ZIP archive of a directory with gitignore support
86
+ */
87
+ async function zipRepository(directoryPath, options = {}) {
88
+ const maxSizeBytes = options.maxSizeBytes || 500 * 1024 * 1024; // 500MB default
89
+ // Validate directory exists
90
+ try {
91
+ const stats = await fs_1.promises.stat(directoryPath);
92
+ if (!stats.isDirectory()) {
93
+ throw new Error(`Path is not a directory: ${directoryPath}`);
94
+ }
95
+ }
96
+ catch (error) {
97
+ if (error.code === 'ENOENT') {
98
+ throw new Error(`Directory does not exist: ${directoryPath}`);
99
+ }
100
+ if (error.code === 'EACCES') {
101
+ throw new Error(`Permission denied accessing directory: ${directoryPath}`);
102
+ }
103
+ throw error;
104
+ }
105
+ // Parse gitignore files
106
+ const ignoreFilter = await buildIgnoreFilter(directoryPath, options.additionalExclusions);
107
+ // Create temp file path
108
+ const tempDir = (0, os_1.tmpdir)();
109
+ const zipFileName = `supermodel-${(0, crypto_1.randomBytes)(8).toString('hex')}.zip`;
110
+ const zipPath = (0, path_1.join)(tempDir, zipFileName);
111
+ console.error('[DEBUG] Creating ZIP:', zipPath);
112
+ console.error('[DEBUG] Source directory:', directoryPath);
113
+ // Create ZIP archive
114
+ let fileCount = 0;
115
+ let totalSize = 0;
116
+ const output = (0, fs_1.createWriteStream)(zipPath);
117
+ const archive = (0, archiver_1.default)('zip', {
118
+ zlib: { level: 6 } // Balanced compression
119
+ });
120
+ // Track errors
121
+ let archiveError = null;
122
+ archive.on('error', (err) => {
123
+ archiveError = err;
124
+ });
125
+ archive.on('warning', (err) => {
126
+ if (err.code === 'ENOENT') {
127
+ console.error('[WARN] File not found (skipping):', err.message);
128
+ }
129
+ else {
130
+ console.error('[WARN] Archive warning:', err.message);
131
+ }
132
+ });
133
+ // Track progress
134
+ archive.on('entry', (entry) => {
135
+ fileCount++;
136
+ totalSize += entry.stats?.size || 0;
137
+ // Check size limit
138
+ if (totalSize > maxSizeBytes) {
139
+ archive.abort();
140
+ archiveError = new Error(`ZIP size exceeds limit (${formatBytes(maxSizeBytes)}). ` +
141
+ `Current size: ${formatBytes(totalSize)}. ` +
142
+ `Consider excluding more directories or analyzing a subdirectory.`);
143
+ }
144
+ });
145
+ // Pipe to file
146
+ archive.pipe(output);
147
+ // Add files recursively with filtering
148
+ await addFilesRecursively(archive, directoryPath, directoryPath, ignoreFilter);
149
+ // Finalize archive
150
+ await archive.finalize();
151
+ // Wait for output stream to finish
152
+ await new Promise((resolve, reject) => {
153
+ output.on('close', () => {
154
+ if (archiveError) {
155
+ reject(archiveError);
156
+ }
157
+ else {
158
+ resolve();
159
+ }
160
+ });
161
+ output.on('error', reject);
162
+ });
163
+ // Check for errors during archiving
164
+ if (archiveError) {
165
+ // Clean up partial ZIP
166
+ await fs_1.promises.unlink(zipPath).catch(() => { });
167
+ throw archiveError;
168
+ }
169
+ // Get final file size
170
+ const zipStats = await fs_1.promises.stat(zipPath);
171
+ const zipSizeBytes = zipStats.size;
172
+ console.error('[DEBUG] ZIP created successfully');
173
+ console.error('[DEBUG] Files included:', fileCount);
174
+ console.error('[DEBUG] ZIP size:', formatBytes(zipSizeBytes));
175
+ // Create cleanup function
176
+ const cleanup = async () => {
177
+ try {
178
+ await fs_1.promises.unlink(zipPath);
179
+ console.error('[DEBUG] Cleaned up ZIP:', zipPath);
180
+ }
181
+ catch (error) {
182
+ if (error.code !== 'ENOENT') {
183
+ console.error('[WARN] Failed to cleanup ZIP:', error.message);
184
+ }
185
+ }
186
+ };
187
+ return {
188
+ path: zipPath,
189
+ cleanup,
190
+ fileCount,
191
+ sizeBytes: zipSizeBytes,
192
+ };
193
+ }
194
+ /**
195
+ * Build ignore filter from .gitignore files and standard exclusions
196
+ */
197
+ async function buildIgnoreFilter(rootDir, additionalExclusions = []) {
198
+ const ig = (0, ignore_1.default)();
199
+ // Add standard exclusions
200
+ ig.add(STANDARD_EXCLUSIONS);
201
+ // Add custom exclusions
202
+ if (additionalExclusions.length > 0) {
203
+ ig.add(additionalExclusions);
204
+ }
205
+ // Parse .gitignore in root
206
+ const gitignorePath = (0, path_1.join)(rootDir, '.gitignore');
207
+ try {
208
+ const gitignoreContent = await fs_1.promises.readFile(gitignorePath, 'utf-8');
209
+ const patterns = gitignoreContent
210
+ .split('\n')
211
+ .map(line => line.trim())
212
+ .filter(line => line && !line.startsWith('#'));
213
+ if (patterns.length > 0) {
214
+ ig.add(patterns);
215
+ console.error('[DEBUG] Loaded .gitignore with', patterns.length, 'patterns');
216
+ }
217
+ }
218
+ catch (error) {
219
+ if (error.code !== 'ENOENT') {
220
+ console.error('[WARN] Failed to read .gitignore:', error.message);
221
+ }
222
+ }
223
+ return ig;
224
+ }
225
+ /**
226
+ * Recursively add files to archive with filtering
227
+ */
228
+ async function addFilesRecursively(archive, rootDir, currentDir, ignoreFilter) {
229
+ let entries;
230
+ try {
231
+ entries = await fs_1.promises.readdir(currentDir);
232
+ }
233
+ catch (error) {
234
+ if (error.code === 'EACCES') {
235
+ console.error('[WARN] Permission denied:', currentDir);
236
+ return;
237
+ }
238
+ throw error;
239
+ }
240
+ for (const entry of entries) {
241
+ const fullPath = (0, path_1.join)(currentDir, entry);
242
+ const relativePath = (0, path_1.relative)(rootDir, fullPath);
243
+ // Normalize path for ignore matching (use forward slashes)
244
+ const normalizedRelativePath = relativePath.split(path_1.sep).join('/');
245
+ // Check if ignored
246
+ if (ignoreFilter.ignores(normalizedRelativePath)) {
247
+ continue;
248
+ }
249
+ let stats;
250
+ try {
251
+ stats = await fs_1.promises.stat(fullPath);
252
+ }
253
+ catch (error) {
254
+ if (error.code === 'ENOENT') {
255
+ // Symlink pointing to non-existent file, skip
256
+ continue;
257
+ }
258
+ console.error('[WARN] Failed to stat:', fullPath, error.message);
259
+ continue;
260
+ }
261
+ if (stats.isDirectory()) {
262
+ // Check if directory itself should be ignored
263
+ const dirPath = normalizedRelativePath + '/';
264
+ if (ignoreFilter.ignores(dirPath)) {
265
+ continue;
266
+ }
267
+ // Recurse into directory
268
+ await addFilesRecursively(archive, rootDir, fullPath, ignoreFilter);
269
+ }
270
+ else if (stats.isFile()) {
271
+ // Add file to archive
272
+ try {
273
+ archive.file(fullPath, { name: normalizedRelativePath });
274
+ }
275
+ catch (error) {
276
+ console.error('[WARN] Failed to add file:', fullPath, error.message);
277
+ }
278
+ }
279
+ // Skip symlinks, sockets, etc.
280
+ }
281
+ }
282
+ /**
283
+ * Format bytes as human-readable string
284
+ */
285
+ function formatBytes(bytes) {
286
+ if (bytes < 1024)
287
+ return `${bytes} B`;
288
+ if (bytes < 1024 * 1024)
289
+ return `${(bytes / 1024).toFixed(2)} KB`;
290
+ if (bytes < 1024 * 1024 * 1024)
291
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
292
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
293
+ }
294
+ /**
295
+ * Clean up old ZIP files from temp directory
296
+ * Removes ZIPs older than the specified age
297
+ */
298
+ async function cleanupOldZips(maxAgeMs = 24 * 60 * 60 * 1000) {
299
+ const tempDir = (0, os_1.tmpdir)();
300
+ const now = Date.now();
301
+ try {
302
+ const entries = await fs_1.promises.readdir(tempDir);
303
+ let removedCount = 0;
304
+ for (const entry of entries) {
305
+ // Only process our ZIP files
306
+ if (!entry.startsWith('supermodel-') || !entry.endsWith('.zip')) {
307
+ continue;
308
+ }
309
+ const fullPath = (0, path_1.join)(tempDir, entry);
310
+ try {
311
+ const stats = await fs_1.promises.stat(fullPath);
312
+ const ageMs = now - stats.mtimeMs;
313
+ if (ageMs > maxAgeMs) {
314
+ await fs_1.promises.unlink(fullPath);
315
+ removedCount++;
316
+ }
317
+ }
318
+ catch (error) {
319
+ // File might have been deleted already, ignore
320
+ if (error.code !== 'ENOENT') {
321
+ console.error('[WARN] Failed to cleanup:', fullPath, error.message);
322
+ }
323
+ }
324
+ }
325
+ if (removedCount > 0) {
326
+ console.error('[DEBUG] Cleaned up', removedCount, 'old ZIP files');
327
+ }
328
+ }
329
+ catch (error) {
330
+ console.error('[WARN] Failed to cleanup temp directory:', error.message);
331
+ }
332
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supermodeltools/mcp-server",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "MCP server for Supermodel API - code graph generation for AI agents",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -36,10 +36,13 @@
36
36
  "dependencies": {
37
37
  "@modelcontextprotocol/sdk": "^1.0.1",
38
38
  "@supermodeltools/sdk": "^0.3.8",
39
+ "archiver": "^7.0.1",
40
+ "ignore": "^7.0.5",
39
41
  "jq-web": "^0.6.2",
40
42
  "zod": "^3.22.4"
41
43
  },
42
44
  "devDependencies": {
45
+ "@types/archiver": "^7.0.0",
43
46
  "@types/node": "^20.11.0",
44
47
  "typescript": "^5.3.3"
45
48
  }