codedev-mcp 3.2.2 → 3.2.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.
@@ -1,7 +1,12 @@
1
1
  import { z } from 'zod';
2
2
  import { outputSchemas } from '../schemas/output-schemas.js';
3
3
  import { CWD, safePath } from '../config.js';
4
- import { searchCode } from '../search/fast-search.js';
4
+ import { searchCode, listFiles } from '../search/fast-search.js';
5
+ import { readFile } from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { parseAST } from '../analyzers/tree-sitter.js';
8
+ import { extractSymbols } from '../analyzers/symbols.js';
9
+ import { detectLanguage } from '../utils/languages.js';
5
10
  /**
6
11
  * Registers code quality tools for finding TODOs, debug logs, secrets, empty catches, duplicates, and dead code.
7
12
  * @param server - The MCP server instance to register tools on.
@@ -21,9 +26,11 @@ export function registerQualityTools(server) {
21
26
  }, async (params) => {
22
27
  try {
23
28
  const searchCwd = params.directory ? safePath(params.directory) : CWD;
29
+ // Only match TODO comments, not function calls like logger.warn()
30
+ // Match TODO/FIXME/HACK/XXX/BUG/OPTIMIZE that appear in comments (// or /* */)
24
31
  const results = await searchCode({
25
32
  cwd: searchCwd,
26
- pattern: '(TODO|FIXME|HACK|XXX|WARN|BUG|OPTIMIZE)\\b',
33
+ pattern: '(//|/\\*|#|<!--).*?(TODO|FIXME|HACK|XXX|BUG|OPTIMIZE)\\b',
27
34
  isRegex: true,
28
35
  fileGlob: params.file_glob,
29
36
  maxResults: 100,
@@ -137,40 +144,94 @@ export function registerQualityTools(server) {
137
144
  }, async (params) => {
138
145
  try {
139
146
  const searchCwd = params.directory ? safePath(params.directory) : CWD;
140
- // Use simpler patterns that work with grep - search for common empty catch patterns
141
- // Pattern 1: catch() { } or catch(e) { }
142
- const catchPattern1 = 'catch[[:space:]]*([^)]*)[[:space:]]*\\{[[:space:]]*\\}';
143
- // Pattern 2: except: pass (Python)
144
- const exceptPattern = 'except:[[:space:]]*pass';
145
- // Pattern 3: rescue => nil (Ruby)
146
- const rescuePattern = 'rescue[[:space:]]*=>[[:space:]]*nil';
147
- const catchResults = await searchCode({
148
- cwd: searchCwd,
149
- pattern: catchPattern1,
150
- isRegex: true,
151
- fileGlob: params.file_glob,
152
- maxResults: 50,
153
- contextLines: 1,
154
- });
155
- const exceptResults = await searchCode({
156
- cwd: searchCwd,
157
- pattern: exceptPattern,
158
- isRegex: true,
159
- fileGlob: params.file_glob,
160
- maxResults: 50,
161
- contextLines: 1,
162
- });
163
- const rescueResults = await searchCode({
164
- cwd: searchCwd,
165
- pattern: rescuePattern,
166
- isRegex: true,
167
- fileGlob: params.file_glob,
168
- maxResults: 50,
169
- contextLines: 1,
170
- });
171
- // Combine and deduplicate results
172
- const allResults = [...catchResults, ...exceptResults, ...rescueResults];
173
- const results = allResults.filter((r, i, arr) => arr.findIndex((other) => other.file === r.file && other.line === r.line) === i);
147
+ const fileGlob = params.file_glob || '**/*.{ts,tsx,js,jsx,py,rb}';
148
+ const files = await listFiles(searchCwd, { glob: fileGlob });
149
+ const matches = [];
150
+ // Process files to find empty catch blocks
151
+ for (const file of files.slice(0, 500)) {
152
+ try {
153
+ const filePath = path.join(searchCwd, file);
154
+ const content = await readFile(filePath, 'utf-8');
155
+ const language = detectLanguage(file);
156
+ if (language === 'python') {
157
+ // Python: except: pass or except Exception: pass
158
+ const exceptRegex = /except\s*(?:\([^)]+\))?\s*:\s*pass/g;
159
+ let match;
160
+ while ((match = exceptRegex.exec(content)) !== null) {
161
+ const lineNum = content.substring(0, match.index).split('\n').length;
162
+ matches.push({
163
+ file,
164
+ line: lineNum,
165
+ match: 'Empty except: pass',
166
+ });
167
+ }
168
+ }
169
+ else if (language === 'ruby') {
170
+ // Ruby: rescue => nil or rescue; end
171
+ const rescueRegex = /rescue\s*(?:=>\s*nil|;?\s*end)/g;
172
+ let match;
173
+ while ((match = rescueRegex.exec(content)) !== null) {
174
+ const lineNum = content.substring(0, match.index).split('\n').length;
175
+ matches.push({
176
+ file,
177
+ line: lineNum,
178
+ match: 'Empty rescue block',
179
+ });
180
+ }
181
+ }
182
+ else {
183
+ // JavaScript/TypeScript: catch() { } or catch(e) { }
184
+ // Find all catch blocks
185
+ const catchRegex = /catch\s*\([^)]*\)\s*\{/g;
186
+ let catchMatch;
187
+ while ((catchMatch = catchRegex.exec(content)) !== null) {
188
+ const catchStart = catchMatch.index;
189
+ const catchLine = content.substring(0, catchStart).split('\n').length;
190
+ const afterCatch = content.substring(catchStart + catchMatch[0].length);
191
+ // Find the matching closing brace
192
+ let braceCount = 1;
193
+ let pos = 0;
194
+ let foundEnd = false;
195
+ while (pos < afterCatch.length && braceCount > 0) {
196
+ if (afterCatch[pos] === '{')
197
+ braceCount++;
198
+ else if (afterCatch[pos] === '}') {
199
+ braceCount--;
200
+ if (braceCount === 0) {
201
+ foundEnd = true;
202
+ break;
203
+ }
204
+ }
205
+ pos++;
206
+ }
207
+ if (foundEnd) {
208
+ const catchBody = afterCatch.substring(0, pos);
209
+ // Check if body is empty (only whitespace/comments)
210
+ const trimmedBody = catchBody
211
+ .replace(/\/\/.*$/gm, '')
212
+ .replace(/\/\*[\s\S]*?\*\//g, '')
213
+ .trim();
214
+ if (trimmedBody === '') {
215
+ matches.push({
216
+ file,
217
+ line: catchLine,
218
+ match: 'Empty catch block',
219
+ });
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ catch {
226
+ continue;
227
+ }
228
+ }
229
+ const results = matches.map((m) => ({
230
+ file: m.file,
231
+ line: m.line,
232
+ text: m.match,
233
+ column: 0,
234
+ }));
174
235
  const output = results.map((r) => `${r.file}:${r.line} │ ${r.text.trim()}`).join('\n');
175
236
  return {
176
237
  content: [{ type: 'text', text: `Found ${results.length} empty/swallowed error handlers:\n\n${output}` }],
@@ -201,16 +262,117 @@ export function registerQualityTools(server) {
201
262
  outputSchema: outputSchemas.find_pattern,
202
263
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
203
264
  }, async (params) => {
204
- const threshold = params.threshold || 50;
205
- return {
206
- content: [
207
- {
208
- type: 'text',
209
- text: `Tip: Use "code_metrics" or "analyze_file" to find functions over ${threshold} lines. These tools use AST parsing for accurate function length detection.`,
265
+ try {
266
+ const threshold = params.threshold || 50;
267
+ const searchCwd = params.directory ? safePath(params.directory) : CWD;
268
+ const fileGlob = params.file_glob || '**/*.{ts,tsx,js,jsx,py,go,java,rs,c,cpp}';
269
+ const files = await listFiles(searchCwd, { glob: fileGlob });
270
+ const matches = [];
271
+ // Increased limit from 500 to 2000 to handle large projects (949+ files)
272
+ for (const file of files.slice(0, 2000)) {
273
+ try {
274
+ const filePath = path.join(searchCwd, file);
275
+ const language = detectLanguage(file);
276
+ const content = await readFile(filePath, 'utf-8');
277
+ const lines = content.split('\n');
278
+ // Try AST parsing first
279
+ const astSymbols = await parseAST(filePath, language);
280
+ if (astSymbols && astSymbols.length > 0) {
281
+ for (const symbol of astSymbols) {
282
+ if (symbol.type === 'function' && symbol.endLine && symbol.startLine) {
283
+ const lineCount = symbol.endLine - symbol.startLine + 1;
284
+ if (lineCount > threshold) {
285
+ matches.push({
286
+ file,
287
+ line: symbol.startLine,
288
+ match: `${symbol.name || 'anonymous'}() - ${lineCount} lines`,
289
+ });
290
+ }
291
+ }
292
+ }
293
+ }
294
+ // Always also try regex-based extraction as fallback/complement
295
+ const symbols = await extractSymbols(filePath);
296
+ const processedFunctions = new Set();
297
+ for (const symbol of symbols.filter((s) => s.kind === 'function')) {
298
+ const key = `${file}:${symbol.line}`;
299
+ if (processedFunctions.has(key))
300
+ continue;
301
+ processedFunctions.add(key);
302
+ // Find function start and end by parsing braces
303
+ let braceCount = 0;
304
+ let startLine = symbol.line;
305
+ let endLine = startLine;
306
+ let foundStart = false;
307
+ // Look backwards to find function start
308
+ for (let i = symbol.line - 1; i >= 0 && i >= symbol.line - 10; i--) {
309
+ const line = lines[i];
310
+ if (/^\s*(?:export\s+)?(?:async\s+)?function\s+\w+|^\s*(?:export\s+)?(?:async\s+)?\w+\s*[:=]\s*(?:async\s*)?\(|^\s*(?:export\s+)?(?:async\s+)?\w+\s*[:=]\s*(?:async\s*)?\w+\s*=>/.test(line)) {
311
+ startLine = i + 1;
312
+ foundStart = true;
313
+ break;
314
+ }
315
+ }
316
+ if (!foundStart)
317
+ startLine = symbol.line;
318
+ // Find function end by counting braces
319
+ for (let i = startLine - 1; i < lines.length; i++) {
320
+ const line = lines[i];
321
+ const openBraces = (line.match(/\{/g) || []).length;
322
+ const closeBraces = (line.match(/\}/g) || []).length;
323
+ if (i === startLine - 1 || braceCount > 0) {
324
+ braceCount += openBraces;
325
+ braceCount -= closeBraces;
326
+ if (braceCount === 0 && i >= startLine) {
327
+ endLine = i + 1;
328
+ break;
329
+ }
330
+ }
331
+ }
332
+ // If we didn't find the end, estimate from remaining content
333
+ if (endLine === startLine && startLine < lines.length) {
334
+ endLine = lines.length;
335
+ }
336
+ const lineCount = endLine - startLine + 1;
337
+ if (lineCount > threshold) {
338
+ // Check if we already added this from AST
339
+ const alreadyAdded = matches.some((m) => m.file === file && Math.abs(m.line - startLine) <= 2);
340
+ if (!alreadyAdded) {
341
+ matches.push({
342
+ file,
343
+ line: startLine,
344
+ match: `${symbol.name}() - ${lineCount} lines`,
345
+ });
346
+ }
347
+ }
348
+ }
349
+ }
350
+ catch {
351
+ // Skip files that can't be parsed, but log for debugging
352
+ continue;
353
+ }
354
+ }
355
+ const output = matches.map((m) => `${m.file}:${m.line} │ ${m.match}`).join('\n');
356
+ return {
357
+ content: [
358
+ {
359
+ type: 'text',
360
+ text: `Found ${matches.length} functions exceeding ${threshold} lines:\n\n${output || 'None found'}`,
361
+ },
362
+ ],
363
+ structuredContent: {
364
+ check: 'long_functions',
365
+ matches,
366
+ total: matches.length,
210
367
  },
211
- ],
212
- structuredContent: { check: 'long_functions', matches: [], total: 0 },
213
- };
368
+ };
369
+ }
370
+ catch (error) {
371
+ return {
372
+ content: [{ type: 'text', text: `find_long_functions failed: ${error.message}` }],
373
+ structuredContent: { check: 'long_functions', matches: [], total: 0 },
374
+ };
375
+ }
214
376
  });
215
377
  // ═══════════════════════════════════════════════════════════════════════
216
378
  // TOOL: find_large_files — Find overly large files
@@ -225,16 +387,63 @@ export function registerQualityTools(server) {
225
387
  outputSchema: outputSchemas.find_pattern,
226
388
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
227
389
  }, async (params) => {
228
- const threshold = params.threshold || 500;
229
- return {
230
- content: [
231
- {
232
- type: 'text',
233
- text: `Tip: Use "codebase_map" or "file_tree" to see file sizes. Use "code_metrics" with a threshold of ${threshold} to identify large files programmatically.`,
390
+ try {
391
+ const threshold = params.threshold || 500;
392
+ const searchCwd = params.directory ? safePath(params.directory) : CWD;
393
+ const fileGlob = params.file_glob || '**/*';
394
+ const files = await listFiles(searchCwd, { glob: fileGlob });
395
+ const matches = [];
396
+ // Process more files and ensure we're scanning the right directory
397
+ // Increased limit from 1000 to 2000 to handle large projects (949+ files)
398
+ for (const file of files.slice(0, 2000)) {
399
+ try {
400
+ const filePath = path.join(searchCwd, file);
401
+ // Verify file exists and is readable
402
+ const stats = await import('node:fs/promises').then((fs) => fs.stat(filePath));
403
+ if (!stats.isFile())
404
+ continue;
405
+ const content = await readFile(filePath, 'utf-8');
406
+ const lineCount = content.split('\n').length;
407
+ if (lineCount > threshold) {
408
+ matches.push({
409
+ file,
410
+ line: 1,
411
+ match: `${lineCount} lines`,
412
+ });
413
+ }
414
+ }
415
+ catch {
416
+ // Skip files that can't be read
417
+ continue;
418
+ }
419
+ }
420
+ // Sort by line count descending
421
+ matches.sort((a, b) => {
422
+ const aLines = parseInt(a.match.match(/\d+/)?.[0] || '0', 10);
423
+ const bLines = parseInt(b.match.match(/\d+/)?.[0] || '0', 10);
424
+ return bLines - aLines;
425
+ });
426
+ const output = matches.map((m) => `${m.file}:${m.match}`).join('\n');
427
+ return {
428
+ content: [
429
+ {
430
+ type: 'text',
431
+ text: `Found ${matches.length} files exceeding ${threshold} lines:\n\n${output || 'None found'}`,
432
+ },
433
+ ],
434
+ structuredContent: {
435
+ check: 'large_files',
436
+ matches,
437
+ total: matches.length,
234
438
  },
235
- ],
236
- structuredContent: { check: 'large_files', matches: [], total: 0 },
237
- };
439
+ };
440
+ }
441
+ catch (error) {
442
+ return {
443
+ content: [{ type: 'text', text: `find_large_files failed: ${error.message}` }],
444
+ structuredContent: { check: 'large_files', matches: [], total: 0 },
445
+ };
446
+ }
238
447
  });
239
448
  // ═══════════════════════════════════════════════════════════════════════
240
449
  // TOOL: find_duplicates — Find duplicate code patterns
@@ -243,15 +452,17 @@ export function registerQualityTools(server) {
243
452
  description: 'Find duplicate code patterns and copy-pasted blocks.',
244
453
  inputSchema: {
245
454
  pattern: z.string().optional().describe('Specific pattern to search for duplicates'),
455
+ min_length: z.number().optional().describe('Minimum length of duplicate block in lines (default: 5)'),
246
456
  file_glob: z.string().optional().describe('Filter by file pattern'),
247
457
  directory: z.string().optional().describe('Subdirectory to search'),
248
458
  },
249
459
  outputSchema: outputSchemas.find_pattern,
250
460
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
251
461
  }, async (params) => {
252
- if (params.pattern) {
253
- try {
254
- const searchCwd = params.directory ? safePath(params.directory) : CWD;
462
+ try {
463
+ const searchCwd = params.directory ? safePath(params.directory) : CWD;
464
+ if (params.pattern) {
465
+ // Pattern-based search
255
466
  const results = await searchCode({
256
467
  cwd: searchCwd,
257
468
  pattern: params.pattern,
@@ -269,22 +480,110 @@ export function registerQualityTools(server) {
269
480
  },
270
481
  };
271
482
  }
272
- catch (error) {
273
- return {
274
- content: [{ type: 'text', text: `find_duplicates failed: ${error.message}` }],
275
- structuredContent: { check: 'duplicates', matches: [], total: 0 },
276
- };
483
+ // AST-based duplicate detection
484
+ const minLength = params.min_length || 5;
485
+ const fileGlob = params.file_glob || '**/*.{ts,tsx,js,jsx,py,go,java,rs}';
486
+ const files = await listFiles(searchCwd, { glob: fileGlob });
487
+ const codeBlocks = new Map();
488
+ const matches = [];
489
+ // Extract function bodies and look for duplicates
490
+ // Increased limit from 100 to 2000 to handle large projects
491
+ for (const file of files.slice(0, 2000)) {
492
+ try {
493
+ const filePath = path.join(searchCwd, file);
494
+ const language = detectLanguage(file);
495
+ const content = await readFile(filePath, 'utf-8');
496
+ const lines = content.split('\n');
497
+ // Try AST parsing first
498
+ const astSymbols = await parseAST(filePath, language);
499
+ if (astSymbols) {
500
+ for (const symbol of astSymbols) {
501
+ if ((symbol.type === 'function' || symbol.type === 'method') && symbol.endLine && symbol.startLine) {
502
+ const lineCount = symbol.endLine - symbol.startLine + 1;
503
+ if (lineCount >= minLength) {
504
+ const body = lines.slice(symbol.startLine - 1, symbol.endLine).join('\n');
505
+ // Normalize whitespace for comparison
506
+ const normalized = body.replace(/\s+/g, ' ').trim();
507
+ if (normalized.length > 50) {
508
+ // Only consider substantial blocks
509
+ const key = normalized.slice(0, 200); // Use first 200 chars as key
510
+ if (!codeBlocks.has(key)) {
511
+ codeBlocks.set(key, []);
512
+ }
513
+ codeBlocks.get(key).push({ file, line: symbol.startLine });
514
+ }
515
+ }
516
+ }
517
+ }
518
+ }
519
+ // Also detect structural patterns (e.g., CRUD patterns)
520
+ // Look for common patterns like: getXById, createX, updateX, deleteX
521
+ // These patterns appear across multiple files with similar structure
522
+ const structuralPatterns = [
523
+ {
524
+ pattern: /(?:export\s+)?(?:const|async\s+function|function)\s+get\w+ById\s*=\s*async\s*\([^)]*id[^)]*\)\s*=>[\s\S]{0,500}?return\s+await\s+db\.query\.\w+\.findFirst/,
525
+ name: 'getXById pattern',
526
+ },
527
+ {
528
+ pattern: /return\s+await\s+db\.query\.\w+\.findFirst\s*\(\s*\{[\s\S]{0,200}?where:\s*\([^)]*\)\s*=>\s*eq\([^)]*\)[\s\S]{0,200}?\}\)/,
529
+ name: 'db.query.findFirst with eq pattern',
530
+ },
531
+ ];
532
+ for (const { pattern, name } of structuralPatterns) {
533
+ const patternMatches = content.matchAll(pattern);
534
+ for (const match of patternMatches) {
535
+ const line = content.substring(0, match.index).split('\n').length;
536
+ const key = `structural:${name}`;
537
+ if (!codeBlocks.has(key)) {
538
+ codeBlocks.set(key, []);
539
+ }
540
+ codeBlocks.get(key).push({ file, line });
541
+ }
542
+ }
543
+ }
544
+ catch {
545
+ continue;
546
+ }
277
547
  }
278
- }
279
- return {
280
- content: [
281
- {
282
- type: 'text',
283
- text: 'Tip: Use "search_code" to find repeated patterns. Provide a specific pattern to this tool to count occurrences across the codebase.',
548
+ // Find duplicates (appearing in 2+ files)
549
+ for (const [, locations] of codeBlocks.entries()) {
550
+ if (locations.length >= 2) {
551
+ const uniqueFiles = new Set(locations.map((l) => l.file));
552
+ if (uniqueFiles.size >= 2) {
553
+ for (const loc of locations) {
554
+ matches.push({
555
+ file: loc.file,
556
+ line: loc.line,
557
+ match: `Duplicate code block (found in ${locations.length} locations)`,
558
+ });
559
+ }
560
+ }
561
+ }
562
+ }
563
+ const output = matches
564
+ .slice(0, 50)
565
+ .map((m) => `${m.file}:${m.line} │ ${m.match}`)
566
+ .join('\n');
567
+ return {
568
+ content: [
569
+ {
570
+ type: 'text',
571
+ text: `Found ${matches.length} duplicate code blocks (min ${minLength} lines):\n\n${output || 'None found'}`,
572
+ },
573
+ ],
574
+ structuredContent: {
575
+ check: 'duplicates',
576
+ matches: matches.slice(0, 100),
577
+ total: matches.length,
284
578
  },
285
- ],
286
- structuredContent: { check: 'duplicates', matches: [], total: 0 },
287
- };
579
+ };
580
+ }
581
+ catch (error) {
582
+ return {
583
+ content: [{ type: 'text', text: `find_duplicates failed: ${error.message}` }],
584
+ structuredContent: { check: 'duplicates', matches: [], total: 0 },
585
+ };
586
+ }
288
587
  });
289
588
  // ═══════════════════════════════════════════════════════════════════════
290
589
  // TOOL: find_dead_code — Find unused exports
@@ -297,16 +596,81 @@ export function registerQualityTools(server) {
297
596
  },
298
597
  outputSchema: outputSchemas.find_pattern,
299
598
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
300
- }, async () => {
301
- return {
302
- content: [
303
- {
304
- type: 'text',
305
- text: 'Tip: Use "search_symbols" to find all exports, then use "find_references" on each symbol to check usage. Symbols with 0 references (excluding their definition) are potentially dead code.',
599
+ }, async (params) => {
600
+ try {
601
+ const searchCwd = params.directory ? safePath(params.directory) : CWD;
602
+ const fileGlob = params.file_glob || '**/*.{ts,tsx,js,jsx,py,go,java,rs}';
603
+ const files = await listFiles(searchCwd, { glob: fileGlob });
604
+ const matches = [];
605
+ // Collect all exported symbols
606
+ const exportedSymbols = [];
607
+ // Increased limit from 200 to 2000 to handle large projects
608
+ for (const file of files.slice(0, 2000)) {
609
+ try {
610
+ const filePath = path.join(searchCwd, file);
611
+ const symbols = await extractSymbols(filePath);
612
+ for (const symbol of symbols) {
613
+ // Check if it's exported
614
+ if (symbol.kind === 'export' || symbol.signature?.includes('export')) {
615
+ exportedSymbols.push({
616
+ file,
617
+ name: symbol.name,
618
+ line: symbol.line,
619
+ kind: symbol.kind,
620
+ });
621
+ }
622
+ }
623
+ }
624
+ catch {
625
+ continue;
626
+ }
627
+ }
628
+ // Check references for each exported symbol
629
+ // Increased limit from 100 to 1000 to handle large projects
630
+ for (const symbol of exportedSymbols.slice(0, 1000)) {
631
+ try {
632
+ // Search for references to this symbol
633
+ const references = await searchCode({
634
+ cwd: searchCwd,
635
+ pattern: symbol.name,
636
+ isRegex: false,
637
+ fileGlob,
638
+ wholeWord: true,
639
+ maxResults: 50,
640
+ });
641
+ // Filter out the definition itself
642
+ const externalRefs = references.filter((r) => !(r.file === symbol.file && r.line === symbol.line));
643
+ if (externalRefs.length === 0) {
644
+ matches.push({
645
+ file: symbol.file,
646
+ line: symbol.line,
647
+ match: `Unused export: ${symbol.name} (${symbol.kind})`,
648
+ });
649
+ }
650
+ }
651
+ catch {
652
+ // Skip if reference finding fails
653
+ continue;
654
+ }
655
+ }
656
+ const output = matches.map((m) => `${m.file}:${m.line} │ ${m.match}`).join('\n');
657
+ return {
658
+ content: [
659
+ { type: 'text', text: `Found ${matches.length} potentially unused exports:\n\n${output || 'None found'}` },
660
+ ],
661
+ structuredContent: {
662
+ check: 'dead_code',
663
+ matches,
664
+ total: matches.length,
306
665
  },
307
- ],
308
- structuredContent: { check: 'dead_code', matches: [], total: 0 },
309
- };
666
+ };
667
+ }
668
+ catch (error) {
669
+ return {
670
+ content: [{ type: 'text', text: `find_dead_code failed: ${error.message}` }],
671
+ structuredContent: { check: 'dead_code', matches: [], total: 0 },
672
+ };
673
+ }
310
674
  });
311
675
  }
312
676
  //# sourceMappingURL=quality.js.map