@reverse-craft/smart-fs 1.0.2 → 1.0.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.
package/dist/server.js CHANGED
@@ -1,22 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/server.ts
4
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import {
7
- CallToolRequestSchema,
8
- ListToolsRequestSchema
9
- } from "@modelcontextprotocol/sdk/types.js";
6
+ import { z as z5 } from "zod";
7
+
8
+ // src/tools/readCodeSmart.ts
10
9
  import { z } from "zod";
11
10
  import { SourceMapConsumer } from "source-map-js";
12
11
 
12
+ // src/tools/ToolDefinition.ts
13
+ function defineTool(definition) {
14
+ return definition;
15
+ }
16
+
13
17
  // src/beautifier.ts
14
18
  import * as esbuild from "esbuild";
15
19
  import * as crypto from "crypto";
16
20
  import * as fs from "fs/promises";
17
21
  import * as path from "path";
18
22
  import * as os from "os";
19
- var TEMP_DIR = path.join(os.tmpdir(), "jsvmp-mcp-cache");
23
+ var TEMP_DIR = path.join(os.tmpdir(), "smart-fs-mcp-cache");
20
24
  async function ensureCacheDir() {
21
25
  await fs.mkdir(TEMP_DIR, { recursive: true });
22
26
  }
@@ -30,6 +34,15 @@ function getCachePaths(originalPath, hash) {
30
34
  const mapPath = `${beautifiedPath}.map`;
31
35
  return { beautifiedPath, mapPath };
32
36
  }
37
+ function getLocalPaths(originalPath) {
38
+ const absolutePath = path.resolve(originalPath);
39
+ const dir = path.dirname(absolutePath);
40
+ const ext = path.extname(absolutePath);
41
+ const baseName = path.basename(absolutePath, ext);
42
+ const beautifiedPath = path.join(dir, `${baseName}.beautified.js`);
43
+ const mapPath = `${beautifiedPath}.map`;
44
+ return { beautifiedPath, mapPath };
45
+ }
33
46
  async function isCacheValid(beautifiedPath, mapPath) {
34
47
  try {
35
48
  await Promise.all([
@@ -41,14 +54,69 @@ async function isCacheValid(beautifiedPath, mapPath) {
41
54
  return false;
42
55
  }
43
56
  }
44
- async function ensureBeautified(originalPath) {
57
+ async function isLocalCacheValid(originalPath) {
45
58
  const absolutePath = path.resolve(originalPath);
59
+ const { beautifiedPath } = getLocalPaths(absolutePath);
60
+ let originalStats;
61
+ try {
62
+ originalStats = await fs.stat(absolutePath);
63
+ } catch {
64
+ return {
65
+ originalMtime: 0,
66
+ beautifiedExists: false,
67
+ beautifiedMtime: 0,
68
+ isValid: false
69
+ };
70
+ }
71
+ const originalMtime = originalStats.mtimeMs;
72
+ let beautifiedStats;
73
+ try {
74
+ beautifiedStats = await fs.stat(beautifiedPath);
75
+ } catch {
76
+ return {
77
+ originalMtime,
78
+ beautifiedExists: false,
79
+ beautifiedMtime: 0,
80
+ isValid: false
81
+ };
82
+ }
83
+ const beautifiedMtime = beautifiedStats.mtimeMs;
84
+ const isValid = beautifiedMtime >= originalMtime;
85
+ return {
86
+ originalMtime,
87
+ beautifiedExists: true,
88
+ beautifiedMtime,
89
+ isValid
90
+ };
91
+ }
92
+ async function ensureBeautified(originalPath, options) {
93
+ const absolutePath = path.resolve(originalPath);
94
+ const saveLocal = options?.saveLocal ?? false;
46
95
  let stats;
47
96
  try {
48
97
  stats = await fs.stat(absolutePath);
49
98
  } catch {
50
99
  throw new Error(`File not found: ${originalPath}`);
51
100
  }
101
+ const localPaths = getLocalPaths(absolutePath);
102
+ if (saveLocal) {
103
+ const localCacheCheck = await isLocalCacheValid(absolutePath);
104
+ if (localCacheCheck.isValid) {
105
+ try {
106
+ const [code2, mapContent] = await Promise.all([
107
+ fs.readFile(localPaths.beautifiedPath, "utf-8"),
108
+ fs.readFile(localPaths.mapPath, "utf-8")
109
+ ]);
110
+ return {
111
+ code: code2,
112
+ rawMap: JSON.parse(mapContent),
113
+ localPath: localPaths.beautifiedPath,
114
+ localMapPath: localPaths.mapPath
115
+ };
116
+ } catch {
117
+ }
118
+ }
119
+ }
52
120
  await ensureCacheDir();
53
121
  const hash = calculateCacheKey(absolutePath, stats.mtimeMs);
54
122
  const { beautifiedPath, mapPath } = getCachePaths(absolutePath, hash);
@@ -57,14 +125,18 @@ async function ensureBeautified(originalPath) {
57
125
  fs.readFile(beautifiedPath, "utf-8"),
58
126
  fs.readFile(mapPath, "utf-8")
59
127
  ]);
60
- return {
128
+ const result2 = {
61
129
  code: code2,
62
130
  rawMap: JSON.parse(mapContent)
63
131
  };
132
+ if (saveLocal) {
133
+ await saveToLocal(result2, localPaths, mapContent);
134
+ }
135
+ return result2;
64
136
  }
65
- let result;
137
+ let esbuildResult;
66
138
  try {
67
- result = await esbuild.build({
139
+ esbuildResult = await esbuild.build({
68
140
  entryPoints: [absolutePath],
69
141
  bundle: false,
70
142
  write: false,
@@ -81,18 +153,42 @@ async function ensureBeautified(originalPath) {
81
153
  const message = err instanceof Error ? err.message : String(err);
82
154
  throw new Error(`Esbuild processing failed: ${message}`);
83
155
  }
84
- const codeFile = result.outputFiles?.find((f) => f.path.endsWith(".js"));
85
- const mapFile = result.outputFiles?.find((f) => f.path.endsWith(".map"));
156
+ const codeFile = esbuildResult.outputFiles?.find((f) => f.path.endsWith(".js"));
157
+ const mapFile = esbuildResult.outputFiles?.find((f) => f.path.endsWith(".map"));
86
158
  if (!codeFile || !mapFile) {
87
159
  throw new Error("Esbuild processing failed: Missing output files");
88
160
  }
89
161
  const code = codeFile.text;
90
162
  const rawMap = JSON.parse(mapFile.text);
163
+ const mapText = mapFile.text;
91
164
  await Promise.all([
92
165
  fs.writeFile(beautifiedPath, code, "utf-8"),
93
- fs.writeFile(mapPath, mapFile.text, "utf-8")
166
+ fs.writeFile(mapPath, mapText, "utf-8")
94
167
  ]);
95
- return { code, rawMap };
168
+ const result = { code, rawMap };
169
+ if (saveLocal) {
170
+ await saveToLocal(result, localPaths, mapText);
171
+ }
172
+ return result;
173
+ }
174
+ async function saveToLocal(result, localPaths, mapText) {
175
+ try {
176
+ await Promise.all([
177
+ fs.writeFile(localPaths.beautifiedPath, result.code, "utf-8"),
178
+ fs.writeFile(localPaths.mapPath, mapText, "utf-8")
179
+ ]);
180
+ result.localPath = localPaths.beautifiedPath;
181
+ result.localMapPath = localPaths.mapPath;
182
+ } catch (err) {
183
+ const error = err;
184
+ if (error.code === "EACCES" || error.code === "EPERM") {
185
+ result.localSaveError = `Permission denied: Cannot write to ${path.dirname(localPaths.beautifiedPath)}`;
186
+ } else if (error.code === "ENOSPC") {
187
+ result.localSaveError = `Insufficient disk space: Cannot write to ${path.dirname(localPaths.beautifiedPath)}`;
188
+ } else {
189
+ result.localSaveError = `Failed to save locally: ${error.message || String(err)}`;
190
+ }
191
+ }
96
192
  }
97
193
 
98
194
  // src/truncator.ts
@@ -157,25 +253,26 @@ function truncateCodeHighPerf(sourceCode, limit = 200, previewLength = 50) {
157
253
  });
158
254
  return magicString.toString();
159
255
  }
160
-
161
- // src/server.ts
162
- var ReadCodeSmartInputSchema = z.object({
163
- file_path: z.string().describe("Path to the JavaScript file"),
164
- start_line: z.number().int().min(1).describe("Start line number (1-based)"),
165
- end_line: z.number().int().min(1).describe("End line number (1-based)"),
166
- char_limit: z.number().int().min(50).default(300).describe("Character limit for string truncation")
167
- });
168
- var server = new Server(
169
- {
170
- name: "jsvmp-smart-fs",
171
- version: "1.0.0"
172
- },
173
- {
174
- capabilities: {
175
- tools: {}
176
- }
256
+ function truncateLongLines(code, maxLineChars = 500, previewRatio = 0.2) {
257
+ if (!code) {
258
+ return code;
177
259
  }
178
- );
260
+ const lines = code.split("\n");
261
+ const previewLength = Math.floor(maxLineChars * previewRatio);
262
+ const processedLines = lines.map((line) => {
263
+ if (line.length <= maxLineChars) {
264
+ return line;
265
+ }
266
+ const start = line.slice(0, previewLength);
267
+ const end = line.slice(-previewLength);
268
+ const truncatedChars = line.length - previewLength * 2;
269
+ const marker = `...[LINE TRUNCATED ${truncatedChars} CHARS]...`;
270
+ return `${start}${marker}${end}`;
271
+ });
272
+ return processedLines.join("\n");
273
+ }
274
+
275
+ // src/tools/readCodeSmart.ts
179
276
  function formatSourcePosition(line, column) {
180
277
  if (line !== null && column !== null) {
181
278
  return `L${line}:${column}`;
@@ -200,95 +297,716 @@ function formatPaginationHint(nextStartLine) {
200
297
  return `
201
298
  ... (Use next start_line=${nextStartLine} to read more)`;
202
299
  }
203
- async function handleReadCodeSmart(input) {
204
- const { file_path, start_line, end_line, char_limit } = input;
205
- const { code, rawMap } = await ensureBeautified(file_path);
206
- const truncatedCode = truncateCodeHighPerf(code, char_limit);
207
- const lines = truncatedCode.split("\n");
208
- const totalLines = lines.length;
209
- const effectiveStartLine = Math.max(1, start_line);
210
- const effectiveEndLine = Math.min(totalLines, end_line);
211
- if (effectiveStartLine > totalLines) {
212
- throw new Error(`Start line ${start_line} exceeds total lines ${totalLines}`);
300
+ var readCodeSmart = defineTool({
301
+ name: "read_code_smart",
302
+ description: "Read and beautify minified/obfuscated JavaScript code with source map coordinates. Returns formatted code with original file positions for setting breakpoints. Optionally saves the beautified file locally alongside the original file.",
303
+ schema: {
304
+ file_path: z.string().describe("Path to the JavaScript file"),
305
+ start_line: z.number().int().min(1).describe("Start line number (1-based)"),
306
+ end_line: z.number().int().min(1).describe("End line number (1-based)"),
307
+ char_limit: z.number().int().min(50).default(300).describe("Character limit for string truncation"),
308
+ max_line_chars: z.number().int().min(80).default(500).describe("Maximum characters per line"),
309
+ save_local: z.boolean().optional().default(false).describe("Save beautified file to the same directory as the original file")
310
+ },
311
+ handler: async (params) => {
312
+ const { file_path, start_line, end_line, char_limit, max_line_chars, save_local } = params;
313
+ const beautifyResult = await ensureBeautified(file_path, { saveLocal: save_local });
314
+ const { code, rawMap, localPath, localMapPath, localSaveError } = beautifyResult;
315
+ const truncatedCode = truncateCodeHighPerf(code, char_limit);
316
+ const lineTruncatedCode = truncateLongLines(truncatedCode, max_line_chars);
317
+ const lines = lineTruncatedCode.split("\n");
318
+ const totalLines = lines.length;
319
+ const effectiveStartLine = Math.max(1, start_line);
320
+ const effectiveEndLine = Math.min(totalLines, end_line);
321
+ if (effectiveStartLine > totalLines) {
322
+ throw new Error(`Start line ${start_line} exceeds total lines ${totalLines}`);
323
+ }
324
+ const consumer = new SourceMapConsumer({
325
+ ...rawMap,
326
+ version: String(rawMap.version)
327
+ });
328
+ const outputParts = [];
329
+ outputParts.push(formatHeader(file_path, effectiveStartLine, effectiveEndLine, totalLines));
330
+ if (save_local) {
331
+ if (localPath) {
332
+ outputParts.push(`LOCAL: ${localPath}`);
333
+ if (localMapPath) {
334
+ outputParts.push(`LOCAL_MAP: ${localMapPath}`);
335
+ }
336
+ }
337
+ if (localSaveError) {
338
+ outputParts.push(`LOCAL_SAVE_ERROR: ${localSaveError}`);
339
+ }
340
+ outputParts.push("-".repeat(85));
341
+ }
342
+ const maxLineNumWidth = String(effectiveEndLine).length;
343
+ for (let lineNum = effectiveStartLine; lineNum <= effectiveEndLine; lineNum++) {
344
+ const lineIndex = lineNum - 1;
345
+ const lineContent = lines[lineIndex] ?? "";
346
+ const originalPos = consumer.originalPositionFor({
347
+ line: lineNum,
348
+ column: 0
349
+ });
350
+ const sourcePos = formatSourcePosition(originalPos.line, originalPos.column);
351
+ outputParts.push(formatCodeLine(lineNum, sourcePos, lineContent, maxLineNumWidth));
352
+ }
353
+ if (effectiveEndLine < totalLines) {
354
+ outputParts.push(formatPaginationHint(effectiveEndLine + 1));
355
+ }
356
+ return outputParts.join("\n");
213
357
  }
214
- const consumer = new SourceMapConsumer({
358
+ });
359
+
360
+ // src/tools/applyCustomTransform.ts
361
+ import { z as z2 } from "zod";
362
+
363
+ // src/transformer.ts
364
+ import * as path2 from "path";
365
+ import * as fs2 from "fs/promises";
366
+ import { transformSync } from "@babel/core";
367
+ function cleanBasename(filename) {
368
+ const base = path2.basename(filename);
369
+ let name = base.endsWith(".js") ? base.slice(0, -3) : base;
370
+ name = name.replace(/_deob[^/]*$/, "");
371
+ if (name.endsWith(".beautified")) {
372
+ name = name.slice(0, -".beautified".length);
373
+ }
374
+ return name;
375
+ }
376
+ function getOutputPaths(targetFile, outputSuffix = "_deob") {
377
+ const absolutePath = path2.resolve(targetFile);
378
+ const dir = path2.dirname(absolutePath);
379
+ const basename3 = cleanBasename(absolutePath);
380
+ const outputPath = path2.join(dir, `${basename3}${outputSuffix}.js`);
381
+ const mapPath = `${outputPath}.map`;
382
+ return { outputPath, mapPath };
383
+ }
384
+ async function loadBabelPlugin(scriptPath) {
385
+ const absolutePath = path2.resolve(scriptPath);
386
+ try {
387
+ await fs2.access(absolutePath);
388
+ } catch {
389
+ throw new Error(`Script not found: ${absolutePath}`);
390
+ }
391
+ const fileUrl = `file://${absolutePath}?t=${Date.now()}`;
392
+ let module;
393
+ try {
394
+ module = await import(fileUrl);
395
+ } catch (err) {
396
+ const message = err instanceof Error ? err.message : String(err);
397
+ throw new Error(`Failed to load script: ${message}`);
398
+ }
399
+ const plugin = module.default ?? module;
400
+ if (typeof plugin !== "function") {
401
+ throw new Error(
402
+ `Invalid Babel plugin: Script must export a function that returns a visitor object. Got ${typeof plugin} instead.`
403
+ );
404
+ }
405
+ return plugin;
406
+ }
407
+ function runBabelTransform(code, inputSourceMap, plugin, filename) {
408
+ let result;
409
+ try {
410
+ result = transformSync(code, {
411
+ filename,
412
+ plugins: [plugin],
413
+ // Source map configuration for cascade
414
+ // @ts-expect-error - SourceMap is compatible with InputSourceMap at runtime
415
+ inputSourceMap,
416
+ sourceMaps: true,
417
+ // Readability settings
418
+ retainLines: false,
419
+ compact: false,
420
+ minified: false,
421
+ // Preserve code structure
422
+ parserOpts: {
423
+ sourceType: "unambiguous"
424
+ }
425
+ });
426
+ } catch (err) {
427
+ const message = err instanceof Error ? err.message : String(err);
428
+ throw new Error(`Babel Error: ${message}`);
429
+ }
430
+ if (!result || !result.code) {
431
+ throw new Error("Babel Error: Transform produced no output");
432
+ }
433
+ if (!result.map) {
434
+ throw new Error("Babel Error: Transform produced no source map");
435
+ }
436
+ return {
437
+ code: result.code,
438
+ map: result.map
439
+ };
440
+ }
441
+ async function applyCustomTransform(targetFile, options) {
442
+ const { scriptPath, outputSuffix = "_deob" } = options;
443
+ const absoluteTargetPath = path2.resolve(targetFile);
444
+ try {
445
+ await fs2.access(absoluteTargetPath);
446
+ } catch {
447
+ throw new Error(`File not found: ${targetFile}`);
448
+ }
449
+ const plugin = await loadBabelPlugin(scriptPath);
450
+ const beautifyResult = await ensureBeautified(absoluteTargetPath);
451
+ const { code: beautifiedCode, rawMap: inputSourceMap } = beautifyResult;
452
+ const transformResult = runBabelTransform(
453
+ beautifiedCode,
454
+ inputSourceMap,
455
+ plugin,
456
+ absoluteTargetPath
457
+ );
458
+ const { outputPath, mapPath } = getOutputPaths(absoluteTargetPath, outputSuffix);
459
+ const mapFileName = path2.basename(mapPath);
460
+ const outputCode = `${transformResult.code}
461
+ //# sourceMappingURL=${mapFileName}`;
462
+ const outputMap = {
463
+ ...transformResult.map,
464
+ file: path2.basename(outputPath)
465
+ };
466
+ try {
467
+ await Promise.all([
468
+ fs2.writeFile(outputPath, outputCode, "utf-8"),
469
+ fs2.writeFile(mapPath, JSON.stringify(outputMap, null, 2), "utf-8")
470
+ ]);
471
+ } catch (err) {
472
+ const error = err;
473
+ if (error.code === "EACCES" || error.code === "EPERM") {
474
+ throw new Error(`Permission denied: Cannot write to ${path2.dirname(outputPath)}`);
475
+ }
476
+ throw new Error(`Failed to write output files: ${error.message || String(err)}`);
477
+ }
478
+ return {
479
+ code: outputCode,
480
+ map: outputMap,
481
+ outputPath,
482
+ mapPath
483
+ };
484
+ }
485
+
486
+ // src/tools/applyCustomTransform.ts
487
+ var ApplyCustomTransformInputSchema = z2.object({
488
+ target_file: z2.string().describe("Path to the JavaScript file to transform"),
489
+ script_path: z2.string().describe("Path to a JS file exporting a Babel Plugin function"),
490
+ output_suffix: z2.string().default("_deob").describe("Suffix for output file name")
491
+ });
492
+ var applyCustomTransform2 = defineTool({
493
+ name: "apply_custom_transform",
494
+ description: "Apply a custom Babel transformation to deobfuscate JavaScript code. Takes a target JS file and a Babel plugin script, runs the transformation, and outputs the deobfuscated code with a cascaded source map that traces back to the original minified file. The plugin script should export a function that returns a Babel visitor object.",
495
+ schema: {
496
+ target_file: z2.string().describe("Path to the JavaScript file to transform"),
497
+ script_path: z2.string().describe("Path to a JS file exporting a Babel Plugin function"),
498
+ output_suffix: z2.string().default("_deob").describe("Suffix for output file name")
499
+ },
500
+ handler: async (params) => {
501
+ const { target_file, script_path, output_suffix } = params;
502
+ const result = await applyCustomTransform(target_file, {
503
+ scriptPath: script_path,
504
+ outputSuffix: output_suffix
505
+ });
506
+ const successMessage = [
507
+ "Transform completed successfully!",
508
+ "",
509
+ `Output file: ${result.outputPath}`,
510
+ `Source map: ${result.mapPath}`
511
+ ].join("\n");
512
+ return successMessage;
513
+ }
514
+ });
515
+
516
+ // src/tools/searchCodeSmart.ts
517
+ import { z as z3 } from "zod";
518
+
519
+ // src/searcher.ts
520
+ import { SourceMapConsumer as SourceMapConsumer2 } from "source-map-js";
521
+ function createRegex(query, caseSensitive = false) {
522
+ try {
523
+ const flags = caseSensitive ? "g" : "gi";
524
+ return new RegExp(query, flags);
525
+ } catch (err) {
526
+ const message = err instanceof Error ? err.message : String(err);
527
+ throw new Error(`Invalid regex: ${message}`);
528
+ }
529
+ }
530
+ function getOriginalPosition(consumer, lineNumber) {
531
+ const pos = consumer.originalPositionFor({
532
+ line: lineNumber,
533
+ column: 0
534
+ });
535
+ return {
536
+ line: pos.line,
537
+ column: pos.column
538
+ };
539
+ }
540
+ function createContextLine(lineNumber, content, consumer) {
541
+ return {
542
+ lineNumber,
543
+ content,
544
+ originalPosition: getOriginalPosition(consumer, lineNumber)
545
+ };
546
+ }
547
+ function searchInCode(code, rawMap, options) {
548
+ const {
549
+ query,
550
+ contextLines = 2,
551
+ caseSensitive = false,
552
+ maxMatches = 50
553
+ } = options;
554
+ const regex = createRegex(query, caseSensitive);
555
+ const lines = code.split("\n");
556
+ const totalLines = lines.length;
557
+ const consumer = new SourceMapConsumer2({
215
558
  ...rawMap,
216
559
  version: String(rawMap.version)
217
560
  });
561
+ const matchingLineNumbers = [];
562
+ for (let i = 0; i < totalLines; i++) {
563
+ const line = lines[i];
564
+ regex.lastIndex = 0;
565
+ if (regex.test(line)) {
566
+ matchingLineNumbers.push(i + 1);
567
+ }
568
+ }
569
+ const totalMatches = matchingLineNumbers.length;
570
+ const truncated = totalMatches > maxMatches;
571
+ const limitedLineNumbers = matchingLineNumbers.slice(0, maxMatches);
572
+ const matches = limitedLineNumbers.map((lineNumber) => {
573
+ const lineIndex = lineNumber - 1;
574
+ const lineContent = lines[lineIndex];
575
+ const contextBefore = [];
576
+ for (let i = Math.max(0, lineIndex - contextLines); i < lineIndex; i++) {
577
+ contextBefore.push(createContextLine(i + 1, lines[i], consumer));
578
+ }
579
+ const contextAfter = [];
580
+ for (let i = lineIndex + 1; i <= Math.min(totalLines - 1, lineIndex + contextLines); i++) {
581
+ contextAfter.push(createContextLine(i + 1, lines[i], consumer));
582
+ }
583
+ return {
584
+ lineNumber,
585
+ lineContent,
586
+ originalPosition: getOriginalPosition(consumer, lineNumber),
587
+ contextBefore,
588
+ contextAfter
589
+ };
590
+ });
591
+ return {
592
+ matches,
593
+ totalMatches,
594
+ truncated
595
+ };
596
+ }
597
+ function formatSourcePosition2(line, column) {
598
+ if (line !== null && column !== null) {
599
+ return `L${line}:${column}`;
600
+ }
601
+ return "";
602
+ }
603
+ function formatSearchResult(filePath, query, caseSensitive, result, maxMatches = 50) {
604
+ const { matches, totalMatches, truncated } = result;
218
605
  const outputParts = [];
219
- outputParts.push(formatHeader(file_path, effectiveStartLine, effectiveEndLine, totalLines));
220
- const maxLineNumWidth = String(effectiveEndLine).length;
221
- for (let lineNum = effectiveStartLine; lineNum <= effectiveEndLine; lineNum++) {
222
- const lineIndex = lineNum - 1;
223
- const lineContent = lines[lineIndex] ?? "";
224
- const originalPos = consumer.originalPositionFor({
225
- line: lineNum,
226
- column: 0
227
- });
228
- const sourcePos = formatSourcePosition(originalPos.line, originalPos.column);
229
- outputParts.push(formatCodeLine(lineNum, sourcePos, lineContent, maxLineNumWidth));
606
+ const caseInfo = caseSensitive ? "case-sensitive" : "case-insensitive";
607
+ outputParts.push(`FILE: ${filePath}`);
608
+ outputParts.push(`QUERY: "${query}" (${caseInfo})`);
609
+ if (totalMatches === 0) {
610
+ outputParts.push("MATCHES: No matches found");
611
+ return outputParts.join("\n");
612
+ }
613
+ const matchInfo = truncated ? `MATCHES: ${totalMatches} found (showing first ${maxMatches})` : `MATCHES: ${totalMatches} found`;
614
+ outputParts.push(matchInfo);
615
+ outputParts.push("-".repeat(85));
616
+ for (const match of matches) {
617
+ outputParts.push(`--- Match at Line ${match.lineNumber} ---`);
618
+ const allLineNumbers = [
619
+ ...match.contextBefore.map((c) => c.lineNumber),
620
+ match.lineNumber,
621
+ ...match.contextAfter.map((c) => c.lineNumber)
622
+ ];
623
+ const maxLineNumWidth = Math.max(...allLineNumbers.map((n) => String(n).length));
624
+ for (const ctx of match.contextBefore) {
625
+ const lineNumStr = String(ctx.lineNumber).padStart(maxLineNumWidth, " ");
626
+ const srcPos = formatSourcePosition2(ctx.originalPosition.line, ctx.originalPosition.column);
627
+ const srcPosPadded = srcPos ? `Src ${srcPos}` : "";
628
+ outputParts.push(` ${lineNumStr} | [${srcPosPadded.padEnd(14, " ")}] | ${ctx.content}`);
629
+ }
630
+ const matchLineNumStr = String(match.lineNumber).padStart(maxLineNumWidth, " ");
631
+ const matchSrcPos = formatSourcePosition2(match.originalPosition.line, match.originalPosition.column);
632
+ const matchSrcPosPadded = matchSrcPos ? `Src ${matchSrcPos}` : "";
633
+ outputParts.push(`>> ${matchLineNumStr} | [${matchSrcPosPadded.padEnd(14, " ")}] | ${match.lineContent}`);
634
+ for (const ctx of match.contextAfter) {
635
+ const lineNumStr = String(ctx.lineNumber).padStart(maxLineNumWidth, " ");
636
+ const srcPos = formatSourcePosition2(ctx.originalPosition.line, ctx.originalPosition.column);
637
+ const srcPosPadded = srcPos ? `Src ${srcPos}` : "";
638
+ outputParts.push(` ${lineNumStr} | [${srcPosPadded.padEnd(14, " ")}] | ${ctx.content}`);
639
+ }
640
+ outputParts.push("");
230
641
  }
231
- if (effectiveEndLine < totalLines) {
232
- outputParts.push(formatPaginationHint(effectiveEndLine + 1));
642
+ if (truncated) {
643
+ outputParts.push(`... (${totalMatches - maxMatches} more matches not shown)`);
233
644
  }
234
645
  return outputParts.join("\n");
235
646
  }
236
- server.setRequestHandler(ListToolsRequestSchema, async () => {
647
+
648
+ // src/tools/searchCodeSmart.ts
649
+ var SearchCodeSmartInputSchema = z3.object({
650
+ file_path: z3.string().describe("Path to the JavaScript file"),
651
+ query: z3.string().describe("Regex pattern or text to search"),
652
+ context_lines: z3.number().int().min(0).default(2).describe("Number of context lines"),
653
+ case_sensitive: z3.boolean().default(false).describe("Case sensitive search"),
654
+ char_limit: z3.number().int().min(50).default(300).describe("Character limit for string truncation"),
655
+ max_line_chars: z3.number().int().min(80).default(500).describe("Maximum characters per line")
656
+ });
657
+ var searchCodeSmart = defineTool({
658
+ name: "search_code_smart",
659
+ description: "Search for text or regex patterns in beautified JavaScript code. Returns matching lines with context and original source coordinates for setting breakpoints. Useful for finding code patterns in minified/obfuscated files.",
660
+ schema: {
661
+ file_path: z3.string().describe("Path to the JavaScript file"),
662
+ query: z3.string().describe("Regex pattern or text to search"),
663
+ context_lines: z3.number().int().min(0).default(2).describe("Number of context lines"),
664
+ case_sensitive: z3.boolean().default(false).describe("Case sensitive search"),
665
+ char_limit: z3.number().int().min(50).default(300).describe("Character limit for string truncation"),
666
+ max_line_chars: z3.number().int().min(80).default(500).describe("Maximum characters per line")
667
+ },
668
+ handler: async (params) => {
669
+ const { file_path, query, context_lines, case_sensitive, char_limit, max_line_chars } = params;
670
+ const beautifyResult = await ensureBeautified(file_path);
671
+ const { code, rawMap } = beautifyResult;
672
+ const truncatedCode = truncateCodeHighPerf(code, char_limit);
673
+ const searchResult = searchInCode(truncatedCode, rawMap, {
674
+ query,
675
+ contextLines: context_lines,
676
+ caseSensitive: case_sensitive,
677
+ maxMatches: 50
678
+ });
679
+ let output = formatSearchResult(file_path, query, case_sensitive, searchResult, 50);
680
+ output = truncateLongLines(output, max_line_chars);
681
+ return output;
682
+ }
683
+ });
684
+
685
+ // src/tools/findUsageSmart.ts
686
+ import { z as z4 } from "zod";
687
+
688
+ // src/analyzer.ts
689
+ import { SourceMapConsumer as SourceMapConsumer3 } from "source-map-js";
690
+ import { parse as parse2 } from "@babel/parser";
691
+ var traverse = null;
692
+ async function getTraverse() {
693
+ if (!traverse) {
694
+ const mod = await import("@babel/traverse");
695
+ traverse = mod.default?.default ?? mod.default;
696
+ }
697
+ return traverse;
698
+ }
699
+ var DEFAULT_PARSER_OPTIONS = {
700
+ sourceType: "unambiguous",
701
+ plugins: [
702
+ "jsx",
703
+ "typescript",
704
+ "classProperties",
705
+ "classPrivateProperties",
706
+ "classPrivateMethods",
707
+ "dynamicImport",
708
+ "optionalChaining",
709
+ "nullishCoalescingOperator",
710
+ "objectRestSpread"
711
+ ],
712
+ errorRecovery: true
713
+ };
714
+ function parseCode(code) {
715
+ try {
716
+ return parse2(code, DEFAULT_PARSER_OPTIONS);
717
+ } catch (err) {
718
+ const message = err instanceof Error ? err.message : String(err);
719
+ throw new Error(`Parse error: ${message}`);
720
+ }
721
+ }
722
+ function getOriginalPosition2(consumer, line, column) {
723
+ const pos = consumer.originalPositionFor({ line, column });
724
+ return {
725
+ line: pos.line,
726
+ column: pos.column
727
+ };
728
+ }
729
+ function getLineContent(lines, lineNumber) {
730
+ if (lineNumber < 1 || lineNumber > lines.length) {
731
+ return "";
732
+ }
733
+ return lines[lineNumber - 1];
734
+ }
735
+ function createLocationInfo(line, column, lines, consumer) {
237
736
  return {
238
- tools: [
239
- {
240
- name: "read_code_smart",
241
- description: "Read and beautify minified/obfuscated JavaScript code with source map coordinates. Returns formatted code with original file positions for setting breakpoints.",
242
- inputSchema: {
243
- type: "object",
244
- properties: {
245
- file_path: {
246
- type: "string",
247
- description: "Path to the JavaScript file"
248
- },
249
- start_line: {
250
- type: "number",
251
- description: "Start line number (1-based)"
252
- },
253
- end_line: {
254
- type: "number",
255
- description: "End line number (1-based)"
256
- },
257
- char_limit: {
258
- type: "number",
259
- description: "Character limit for string truncation (default: 300)",
260
- default: 300
261
- }
262
- },
263
- required: ["file_path", "start_line", "end_line"]
737
+ line,
738
+ column,
739
+ originalPosition: getOriginalPosition2(consumer, line, column),
740
+ lineContent: getLineContent(lines, line)
741
+ };
742
+ }
743
+ async function analyzeBindings(code, rawMap, identifier, options) {
744
+ const targetLine = options?.targetLine;
745
+ const isTargeted = targetLine !== void 0;
746
+ const maxReferences = options?.maxReferences ?? (isTargeted ? 15 : 10);
747
+ const ast = parseCode(code);
748
+ const lines = code.split("\n");
749
+ const consumer = new SourceMapConsumer3({
750
+ ...rawMap,
751
+ version: String(rawMap.version)
752
+ });
753
+ const bindings = [];
754
+ const processedScopes = /* @__PURE__ */ new Set();
755
+ const traverse2 = await getTraverse();
756
+ try {
757
+ traverse2(ast, {
758
+ Identifier(path3) {
759
+ if (path3.node.name !== identifier) {
760
+ return;
761
+ }
762
+ if (isTargeted) {
763
+ const nodeLoc = path3.node.loc;
764
+ if (!nodeLoc || nodeLoc.start.line !== targetLine) {
765
+ return;
766
+ }
767
+ }
768
+ const binding = path3.scope.getBinding(identifier);
769
+ if (!binding) {
770
+ return;
771
+ }
772
+ const scopeUid = binding.scope.uid;
773
+ if (processedScopes.has(scopeUid)) {
774
+ return;
775
+ }
776
+ processedScopes.add(scopeUid);
777
+ const defNode = binding.identifier;
778
+ const defLoc = defNode.loc;
779
+ if (!defLoc) {
780
+ return;
781
+ }
782
+ const definition = createLocationInfo(
783
+ defLoc.start.line,
784
+ defLoc.start.column,
785
+ lines,
786
+ consumer
787
+ );
788
+ const allReferences = [];
789
+ for (const refPath of binding.referencePaths) {
790
+ const refLoc = refPath.node.loc;
791
+ if (!refLoc) {
792
+ continue;
793
+ }
794
+ allReferences.push(
795
+ createLocationInfo(
796
+ refLoc.start.line,
797
+ refLoc.start.column,
798
+ lines,
799
+ consumer
800
+ )
801
+ );
802
+ }
803
+ const totalReferences = allReferences.length;
804
+ const limitedReferences = allReferences.slice(0, maxReferences);
805
+ let hitLocation;
806
+ if (isTargeted) {
807
+ const nodeLoc = path3.node.loc;
808
+ hitLocation = createLocationInfo(
809
+ nodeLoc.start.line,
810
+ nodeLoc.start.column,
811
+ lines,
812
+ consumer
813
+ );
814
+ }
815
+ bindings.push({
816
+ scopeUid,
817
+ kind: binding.kind,
818
+ definition,
819
+ references: limitedReferences,
820
+ totalReferences,
821
+ hitLocation
822
+ });
823
+ if (isTargeted) {
824
+ path3.stop();
264
825
  }
265
826
  }
266
- ]
827
+ });
828
+ } catch (err) {
829
+ const message = err instanceof Error ? err.message : String(err);
830
+ throw new Error(`Analysis error: ${message}`);
831
+ }
832
+ return {
833
+ bindings,
834
+ identifier,
835
+ isTargeted,
836
+ targetLine
267
837
  };
268
- });
269
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
270
- const { name, arguments: args } = request.params;
271
- if (name !== "read_code_smart") {
272
- throw new Error(`Tool not found: ${name}`);
838
+ }
839
+ function formatSourcePosition3(line, column) {
840
+ if (line !== null && column !== null) {
841
+ return `L${line}:${column}`;
273
842
  }
274
- try {
275
- const input = ReadCodeSmartInputSchema.parse(args);
276
- const result = await handleReadCodeSmart(input);
277
- return {
278
- content: [{ type: "text", text: result }]
279
- };
280
- } catch (error) {
281
- const message = error instanceof Error ? error.message : String(error);
282
- return {
283
- content: [{ type: "text", text: `Error: ${message}` }],
284
- isError: true
285
- };
843
+ return "";
844
+ }
845
+ function locationsMatch(loc1, loc2) {
846
+ return loc1.line === loc2.line && loc1.column === loc2.column;
847
+ }
848
+ function formatAnalysisResult(filePath, result, maxReferences = 10) {
849
+ const { bindings, identifier, isTargeted, targetLine } = result;
850
+ const outputParts = [];
851
+ outputParts.push(`FILE: ${filePath}`);
852
+ outputParts.push(`IDENTIFIER: "${identifier}"`);
853
+ if (bindings.length === 0) {
854
+ if (isTargeted && targetLine !== void 0) {
855
+ outputParts.push(`BINDINGS: No binding found for "${identifier}" at line ${targetLine}`);
856
+ outputParts.push(`The variable may be global, externally defined, or not present at this line.`);
857
+ } else {
858
+ outputParts.push("BINDINGS: No definitions or references found");
859
+ }
860
+ return outputParts.join("\n");
861
+ }
862
+ if (isTargeted) {
863
+ outputParts.push(`BINDINGS: 1 found (Targeted Scope at line ${targetLine})`);
864
+ } else {
865
+ const scopeInfo = bindings.length > 1 ? " (in different scopes)" : "";
866
+ outputParts.push(`BINDINGS: ${bindings.length} found${scopeInfo}`);
867
+ }
868
+ outputParts.push("-".repeat(85));
869
+ for (let i = 0; i < bindings.length; i++) {
870
+ const binding = bindings[i];
871
+ if (isTargeted) {
872
+ outputParts.push(`=== Targeted Scope (${binding.kind}) ===`);
873
+ } else {
874
+ outputParts.push(`=== Scope #${i + 1} (${binding.kind}) ===`);
875
+ }
876
+ const defIsHit = isTargeted && binding.hitLocation && locationsMatch(binding.definition, binding.hitLocation);
877
+ const defPrefix = defIsHit ? "\u{1F4CD} Definition (hit):" : "\u{1F4CD} Definition:";
878
+ outputParts.push(defPrefix);
879
+ const defSrcPos = formatSourcePosition3(
880
+ binding.definition.originalPosition.line,
881
+ binding.definition.originalPosition.column
882
+ );
883
+ const defSrcPosPadded = defSrcPos ? `Src ${defSrcPos}` : "";
884
+ const defMarker = defIsHit ? " \u25C0\u2500\u2500 hit" : "";
885
+ outputParts.push(
886
+ ` ${binding.definition.line} | [${defSrcPosPadded.padEnd(14, " ")}] | ${binding.definition.lineContent}${defMarker}`
887
+ );
888
+ const totalRefs = binding.totalReferences;
889
+ if (totalRefs === 0) {
890
+ outputParts.push("\u{1F50E} References: None");
891
+ } else {
892
+ outputParts.push(`\u{1F50E} References (${totalRefs}):`);
893
+ for (const ref of binding.references) {
894
+ const refIsHit = isTargeted && binding.hitLocation && locationsMatch(ref, binding.hitLocation);
895
+ const refSrcPos = formatSourcePosition3(
896
+ ref.originalPosition.line,
897
+ ref.originalPosition.column
898
+ );
899
+ const refSrcPosPadded = refSrcPos ? `Src ${refSrcPos}` : "";
900
+ const refMarker = refIsHit ? " \u25C0\u2500\u2500 hit" : "";
901
+ outputParts.push(
902
+ ` ${ref.line} | [${refSrcPosPadded.padEnd(14, " ")}] | ${ref.lineContent}${refMarker}`
903
+ );
904
+ }
905
+ if (totalRefs > maxReferences) {
906
+ const remaining = totalRefs - maxReferences;
907
+ outputParts.push(` ... (${remaining} more references not shown)`);
908
+ }
909
+ }
910
+ outputParts.push("");
911
+ }
912
+ return outputParts.join("\n");
913
+ }
914
+
915
+ // src/tools/findUsageSmart.ts
916
+ var FindUsageSmartInputSchema = z4.object({
917
+ file_path: z4.string().describe("Path to the JavaScript file"),
918
+ identifier: z4.string().describe("Variable or function name to find"),
919
+ line: z4.number().int().positive().optional().describe(
920
+ "The line number where you see this variable. HIGHLY RECOMMENDED for precision in obfuscated code."
921
+ ),
922
+ char_limit: z4.number().int().min(50).default(300).describe("Character limit for string truncation"),
923
+ max_line_chars: z4.number().int().min(80).default(500).describe("Maximum characters per line")
924
+ });
925
+ var findUsageSmart = defineTool({
926
+ name: "find_usage_smart",
927
+ description: "Find all definitions and references of a variable/function using AST scope analysis. Returns binding information grouped by scope with original source coordinates for setting breakpoints. Useful for tracing variable usage in minified/obfuscated code with variable name reuse.",
928
+ schema: {
929
+ file_path: z4.string().describe("Path to the JavaScript file"),
930
+ identifier: z4.string().describe("Variable or function name to find"),
931
+ line: z4.number().int().positive().optional().describe(
932
+ "The line number where you see this variable. HIGHLY RECOMMENDED for precision in obfuscated code."
933
+ ),
934
+ char_limit: z4.number().int().min(50).default(300).describe("Character limit for string truncation"),
935
+ max_line_chars: z4.number().int().min(80).default(500).describe("Maximum characters per line")
936
+ },
937
+ handler: async (params) => {
938
+ const { file_path, identifier, line, char_limit, max_line_chars } = params;
939
+ const beautifyResult = await ensureBeautified(file_path);
940
+ const { code, rawMap } = beautifyResult;
941
+ const analysisResult = await analyzeBindings(code, rawMap, identifier, {
942
+ maxReferences: line ? 15 : 10,
943
+ targetLine: line
944
+ });
945
+ const truncatedCode = truncateCodeHighPerf(code, char_limit);
946
+ const truncatedLines = truncatedCode.split("\n");
947
+ for (const binding of analysisResult.bindings) {
948
+ const defLineIdx = binding.definition.line - 1;
949
+ if (defLineIdx >= 0 && defLineIdx < truncatedLines.length) {
950
+ binding.definition.lineContent = truncatedLines[defLineIdx];
951
+ }
952
+ for (const ref of binding.references) {
953
+ const refLineIdx = ref.line - 1;
954
+ if (refLineIdx >= 0 && refLineIdx < truncatedLines.length) {
955
+ ref.lineContent = truncatedLines[refLineIdx];
956
+ }
957
+ }
958
+ }
959
+ let output = formatAnalysisResult(file_path, analysisResult, line ? 15 : 10);
960
+ output = truncateLongLines(output, max_line_chars);
961
+ return output;
286
962
  }
287
963
  });
964
+
965
+ // src/tools/index.ts
966
+ var tools = [
967
+ readCodeSmart,
968
+ applyCustomTransform2,
969
+ searchCodeSmart,
970
+ findUsageSmart
971
+ ];
972
+
973
+ // src/server.ts
974
+ var server = new McpServer({
975
+ name: "smart-fs",
976
+ version: "1.0.0"
977
+ });
978
+ function registerTool(tool) {
979
+ const zodSchema = z5.object(tool.schema);
980
+ server.registerTool(
981
+ tool.name,
982
+ {
983
+ description: tool.description,
984
+ inputSchema: tool.schema
985
+ },
986
+ async (params, _extra) => {
987
+ try {
988
+ const validatedParams = zodSchema.parse(params);
989
+ const result = await tool.handler(validatedParams);
990
+ return {
991
+ content: [{ type: "text", text: result }]
992
+ };
993
+ } catch (error) {
994
+ const message = error instanceof Error ? error.message : String(error);
995
+ return {
996
+ content: [{ type: "text", text: `Error: ${message}` }],
997
+ isError: true
998
+ };
999
+ }
1000
+ }
1001
+ );
1002
+ }
1003
+ for (const tool of tools) {
1004
+ registerTool(tool);
1005
+ }
288
1006
  async function main() {
289
1007
  const transport = new StdioServerTransport();
290
1008
  await server.connect(transport);
291
- console.error("JSVMP Smart FS MCP Server running on stdio");
1009
+ console.error("Smart FS MCP Server running on stdio");
292
1010
  }
293
1011
  main().catch((error) => {
294
1012
  console.error("Fatal error:", error);