@supermodeltools/mcp-server 0.4.3 → 0.4.5
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.
- package/README.md +27 -13
- package/dist/cache/graph-cache.js +260 -0
- package/dist/cache/graph-types.js +6 -0
- package/dist/cache/index.js +21 -0
- package/dist/queries/discovery.js +148 -0
- package/dist/queries/index.js +241 -0
- package/dist/queries/summary.js +36 -0
- package/dist/queries/traversal.js +392 -0
- package/dist/queries/types.js +38 -0
- package/dist/server.js +38 -1
- package/dist/tools/create-supermodel-graph.js +500 -29
- package/dist/utils/zip-repository.js +332 -0
- package/package.json +4 -1
|
@@ -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
|
+
"version": "0.4.5",
|
|
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
|
}
|