ucn 3.8.18 → 3.8.20

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/mcp/server.js CHANGED
@@ -33,7 +33,7 @@ try {
33
33
  const { ProjectIndex } = require('../core/project');
34
34
  const { findProjectRoot } = require('../core/discovery');
35
35
  const output = require('../core/output');
36
- const { getMcpCommandEnum, normalizeParams, BROAD_COMMANDS: BROAD_CANONICAL, toMcpName } = require('../core/registry');
36
+ const { getMcpCommandEnum, normalizeParams, BROAD_COMMANDS: BROAD_CANONICAL, toMcpName, FLAG_APPLICABILITY, REVERSE_PARAM_MAP, generateMcpParamSection } = require('../core/registry');
37
37
  const { execute } = require('../core/execute');
38
38
  const { ExpandCache } = require('../core/expand-cache');
39
39
 
@@ -123,8 +123,9 @@ const MAX_OUTPUT_CHARS = 100000; // hard ceiling even with max_chars overrid
123
123
  // Broad commands (derived from registry): output is project-wide, truncation means you need a filter
124
124
  const BROAD_COMMANDS = new Set([...BROAD_CANONICAL].map(toMcpName));
125
125
 
126
- function toolResult(text, command, maxChars) {
127
- if (!text) return { content: [{ type: 'text', text: '(no output)' }] };
126
+ function toolResult(text, command, maxChars, suffixNote) {
127
+ const suffix = suffixNote || '';
128
+ if (!text) return { content: [{ type: 'text', text: '(no output)' + suffix }] };
128
129
  const defaultLimit = BROAD_COMMANDS.has(command) ? BROAD_OUTPUT_CHARS : DEFAULT_OUTPUT_CHARS;
129
130
  const limit = Math.min(maxChars || defaultLimit, MAX_OUTPUT_CHARS);
130
131
  if (text.length > limit) {
@@ -144,9 +145,9 @@ function toolResult(text, command, maxChars) {
144
145
  usages: 'Use file= to scope to specific files.',
145
146
  };
146
147
  const narrow = hints[command] || 'Use file=/in=/exclude= to narrow scope.';
147
- return { content: [{ type: 'text', text: cleanCut + `\n\n... OUTPUT TRUNCATED: showing ${limit} of ${fullSize} chars. Full output would be ~${fullTokens} tokens. ${narrow} Or use all=true to see everything (warning: ~${fullTokens} tokens).` }] };
148
+ return { content: [{ type: 'text', text: cleanCut + `\n\n... OUTPUT TRUNCATED: showing ${limit} of ${fullSize} chars. Full output would be ~${fullTokens} tokens. ${narrow} Or use all=true to see everything (warning: ~${fullTokens} tokens).` + suffix }] };
148
149
  }
149
- return { content: [{ type: 'text', text }] };
150
+ return { content: [{ type: 'text', text: text + suffix }] };
150
151
  }
151
152
 
152
153
  function toolError(message) {
@@ -238,7 +239,7 @@ OTHER:
238
239
  - typedef <name>: Find type definitions matching a name: interfaces, enums, structs, traits, type aliases. See field shapes, required methods, or enum values.
239
240
  - stacktrace: Parse a stack trace, show source context per frame. Requires stack param. Handles JS, Python, Go, Rust, Java formats.
240
241
  - api: Public API surface of project or file: all exported/public symbols with signatures. Use to understand what a library exposes. Pass file to scope to one file. Python needs __all__; use toc instead.
241
- - stats: Quick project stats: file counts, symbol counts, lines of code by language and symbol type. Use functions=true for per-function line counts sorted by size (complexity audit).`;
242
+ - stats: Quick project stats: file counts, symbol counts, lines of code by language and symbol type. Use functions=true for per-function line counts sorted by size (complexity audit).` + generateMcpParamSection();
242
243
 
243
244
  server.registerTool(
244
245
  'ucn',
@@ -307,11 +308,39 @@ server.registerTool(
307
308
  // This eliminates per-case param selection and prevents CLI/MCP drift.
308
309
  const { command: _c, project_dir: _p, ...rawParams } = args;
309
310
  const ep = normalizeParams(rawParams);
311
+
312
+ // Strip params not applicable to this command (prevents silent no-ops).
313
+ // Global/core params are always allowed — only optional flags are filtered.
314
+ const strippedParams = [];
315
+ const applicable = FLAG_APPLICABILITY[command];
316
+ if (applicable) {
317
+ // Truly global options — apply to all commands (build/display control).
318
+ // Command-specific params (name, term, stack, range, etc.) are in FLAG_APPLICABILITY.
319
+ const coreParams = new Set(['maxChars', 'maxFiles', 'followSymlinks']);
320
+ for (const key of Object.keys(ep)) {
321
+ if (coreParams.has(key)) continue;
322
+ if (!applicable.includes(key) && ep[key] !== undefined &&
323
+ !(Array.isArray(ep[key]) && ep[key].length === 0)) {
324
+ strippedParams.push(REVERSE_PARAM_MAP[key] || key);
325
+ delete ep[key];
326
+ }
327
+ }
328
+ }
329
+
310
330
  // all=true bypasses both formatter caps AND char truncation (parity with CLI --all)
311
331
  const maxChars = ep.all ? MAX_OUTPUT_CHARS : ep.maxChars;
312
332
 
313
- // Wrap toolResult to auto-inject command + maxChars from this request
314
- const tr = (text) => toolResult(text, command, maxChars);
333
+ // Build stripping note (appended inside truncation boundary on success paths)
334
+ const strippedNote = strippedParams.length > 0
335
+ ? `\n\nNote: ${strippedParams.join(', ')} ignored (not applicable to ${command}).`
336
+ : '';
337
+
338
+ // Wrap toolResult to auto-inject command + maxChars + stripping note
339
+ const tr = (text) => toolResult(text, command, maxChars, strippedNote);
340
+ // Wrap toolError to include stripping note on error paths too
341
+ const te = strippedNote
342
+ ? (msg) => toolError(msg + strippedNote)
343
+ : toolError;
315
344
 
316
345
  let index = null; // Track for post-command cache save
317
346
  try {
@@ -326,7 +355,7 @@ server.registerTool(
326
355
  case 'about': {
327
356
  index = getIndex(project_dir, ep);
328
357
  const { ok, result, error, note } = execute(index, 'about', ep);
329
- if (!ok) return toolError(error);
358
+ if (!ok) return te(error);
330
359
  let aboutText = output.formatAbout(result, {
331
360
  allHint: 'Repeat with all=true to show all.',
332
361
  methodsHint: 'Note: obj.method() callers/callees excluded. Use include_methods=true to include them.',
@@ -339,7 +368,7 @@ server.registerTool(
339
368
  case 'context': {
340
369
  index = getIndex(project_dir, ep);
341
370
  const { ok, result: ctx, error, note } = execute(index, 'context', ep);
342
- if (!ok) return toolError(error);
371
+ if (!ok) return te(error);
343
372
  const { text, expandable } = output.formatContext(ctx, {
344
373
  expandHint: 'Use expand command with item number to see code for any item.',
345
374
  showConfidence: ep.showConfidence !== false,
@@ -353,7 +382,7 @@ server.registerTool(
353
382
  case 'impact': {
354
383
  index = getIndex(project_dir, ep);
355
384
  const { ok, result, error, note } = execute(index, 'impact', ep);
356
- if (!ok) return toolError(error);
385
+ if (!ok) return te(error);
357
386
  let impactText = output.formatImpact(result);
358
387
  if (note) impactText += '\n\n' + note;
359
388
  return tr(impactText);
@@ -362,7 +391,7 @@ server.registerTool(
362
391
  case 'blast': {
363
392
  index = getIndex(project_dir, ep);
364
393
  const { ok, result, error, note } = execute(index, 'blast', ep);
365
- if (!ok) return toolError(error);
394
+ if (!ok) return te(error);
366
395
  let blastText = output.formatBlast(result, {
367
396
  allHint: 'Set depth to expand all children.',
368
397
  });
@@ -373,7 +402,7 @@ server.registerTool(
373
402
  case 'smart': {
374
403
  index = getIndex(project_dir, ep);
375
404
  const { ok, result, error, note } = execute(index, 'smart', ep);
376
- if (!ok) return toolError(error);
405
+ if (!ok) return te(error);
377
406
  let smartText = output.formatSmart(result);
378
407
  if (note) smartText += '\n\n' + note;
379
408
  return tr(smartText);
@@ -382,7 +411,7 @@ server.registerTool(
382
411
  case 'trace': {
383
412
  index = getIndex(project_dir, ep);
384
413
  const { ok, result, error, note } = execute(index, 'trace', ep);
385
- if (!ok) return toolError(error);
414
+ if (!ok) return te(error);
386
415
  let traceText = output.formatTrace(result, {
387
416
  allHint: 'Set depth to expand all children.',
388
417
  methodsHint: 'Note: obj.method() calls excluded. Use include_methods=true to include them.'
@@ -394,7 +423,7 @@ server.registerTool(
394
423
  case 'reverse_trace': {
395
424
  index = getIndex(project_dir, ep);
396
425
  const { ok, result, error, note } = execute(index, 'reverseTrace', ep);
397
- if (!ok) return toolError(error);
426
+ if (!ok) return te(error);
398
427
  let rtText = output.formatReverseTrace(result, {
399
428
  allHint: 'Set depth to expand all children.',
400
429
  });
@@ -405,14 +434,14 @@ server.registerTool(
405
434
  case 'example': {
406
435
  index = getIndex(project_dir, ep);
407
436
  const { ok, result, error } = execute(index, 'example', ep);
408
- if (!ok) return toolError(error);
437
+ if (!ok) return te(error);
409
438
  return tr(output.formatExample(result, ep.name));
410
439
  }
411
440
 
412
441
  case 'related': {
413
442
  index = getIndex(project_dir, ep);
414
443
  const { ok, result, error, note } = execute(index, 'related', ep);
415
- if (!ok) return toolError(error);
444
+ if (!ok) return te(error);
416
445
  let relText = output.formatRelated(result, {
417
446
  all: ep.all || false, top: ep.top,
418
447
  allHint: 'Repeat with all=true to show all.'
@@ -426,7 +455,7 @@ server.registerTool(
426
455
  case 'find': {
427
456
  index = getIndex(project_dir, ep);
428
457
  const { ok, result, error, note } = execute(index, 'find', ep);
429
- if (!ok) return toolError(error);
458
+ if (!ok) return te(error);
430
459
  let text = output.formatFind(result, ep.name, ep.top);
431
460
  if (note) text += '\n\n' + note;
432
461
  return tr(text);
@@ -435,7 +464,7 @@ server.registerTool(
435
464
  case 'usages': {
436
465
  index = getIndex(project_dir, ep);
437
466
  const { ok, result, error, note } = execute(index, 'usages', ep);
438
- if (!ok) return toolError(error);
467
+ if (!ok) return te(error);
439
468
  let text = output.formatUsages(result, ep.name);
440
469
  if (note) text += '\n\n' + note;
441
470
  return tr(text);
@@ -444,7 +473,7 @@ server.registerTool(
444
473
  case 'toc': {
445
474
  index = getIndex(project_dir, ep);
446
475
  const { ok, result, error, note } = execute(index, 'toc', ep);
447
- if (!ok) return toolError(error);
476
+ if (!ok) return te(error);
448
477
  let text = output.formatToc(result, {
449
478
  topHint: 'Set top=N or use detailed=false for compact view.'
450
479
  });
@@ -455,7 +484,7 @@ server.registerTool(
455
484
  case 'search': {
456
485
  index = getIndex(project_dir, ep);
457
486
  const { ok, result, error, structural, note } = execute(index, 'search', ep);
458
- if (!ok) return toolError(error);
487
+ if (!ok) return te(error);
459
488
  let searchText;
460
489
  if (structural) {
461
490
  searchText = output.formatStructuralSearch(result);
@@ -469,7 +498,7 @@ server.registerTool(
469
498
  case 'tests': {
470
499
  index = getIndex(project_dir, ep);
471
500
  const { ok, result, error, note } = execute(index, 'tests', ep);
472
- if (!ok) return toolError(error);
501
+ if (!ok) return te(error);
473
502
  let testsText = output.formatTests(result, ep.name);
474
503
  if (note) testsText += '\n\n' + note;
475
504
  return tr(testsText);
@@ -478,7 +507,7 @@ server.registerTool(
478
507
  case 'affected_tests': {
479
508
  index = getIndex(project_dir, ep);
480
509
  const { ok, result, error, note } = execute(index, 'affectedTests', ep);
481
- if (!ok) return toolError(error);
510
+ if (!ok) return te(error);
482
511
  let atText = output.formatAffectedTests(result, { all: ep.all });
483
512
  if (note) atText += '\n\n' + note;
484
513
  return tr(atText);
@@ -487,7 +516,7 @@ server.registerTool(
487
516
  case 'deadcode': {
488
517
  index = getIndex(project_dir, ep);
489
518
  const { ok, result, error, note } = execute(index, 'deadcode', ep);
490
- if (!ok) return toolError(error);
519
+ if (!ok) return te(error);
491
520
  const dcNote = note;
492
521
  let dcText = output.formatDeadcode(result, {
493
522
  top: ep.top || 0,
@@ -501,7 +530,7 @@ server.registerTool(
501
530
  case 'entrypoints': {
502
531
  index = getIndex(project_dir, ep);
503
532
  const { ok, result, error, note } = execute(index, 'entrypoints', ep);
504
- if (!ok) return toolError(error);
533
+ if (!ok) return te(error);
505
534
  let epText = output.formatEntrypoints(result);
506
535
  if (note) epText += '\n\n' + note;
507
536
  return tr(epText);
@@ -512,28 +541,28 @@ server.registerTool(
512
541
  case 'imports': {
513
542
  index = getIndex(project_dir, ep);
514
543
  const { ok, result, error } = execute(index, 'imports', ep);
515
- if (!ok) return toolError(error);
544
+ if (!ok) return te(error);
516
545
  return tr(output.formatImports(result, ep.file));
517
546
  }
518
547
 
519
548
  case 'exporters': {
520
549
  index = getIndex(project_dir, ep);
521
550
  const { ok, result, error } = execute(index, 'exporters', ep);
522
- if (!ok) return toolError(error);
551
+ if (!ok) return te(error);
523
552
  return tr(output.formatExporters(result, ep.file));
524
553
  }
525
554
 
526
555
  case 'file_exports': {
527
556
  index = getIndex(project_dir, ep);
528
557
  const { ok, result, error } = execute(index, 'fileExports', ep);
529
- if (!ok) return toolError(error);
558
+ if (!ok) return te(error);
530
559
  return tr(output.formatFileExports(result, ep.file));
531
560
  }
532
561
 
533
562
  case 'graph': {
534
563
  index = getIndex(project_dir, ep);
535
564
  const { ok, result, error } = execute(index, 'graph', ep);
536
- if (!ok) return toolError(error);
565
+ if (!ok) return te(error);
537
566
  return tr(output.formatGraph(result, {
538
567
  showAll: ep.all || ep.depth !== undefined,
539
568
  maxDepth: ep.depth ?? 2, file: ep.file,
@@ -545,7 +574,7 @@ server.registerTool(
545
574
  case 'circular_deps': {
546
575
  index = getIndex(project_dir, ep);
547
576
  const { ok, result, error } = execute(index, 'circularDeps', ep);
548
- if (!ok) return toolError(error);
577
+ if (!ok) return te(error);
549
578
  return tr(output.formatCircularDeps(result));
550
579
  }
551
580
 
@@ -554,21 +583,21 @@ server.registerTool(
554
583
  case 'verify': {
555
584
  index = getIndex(project_dir, ep);
556
585
  const { ok, result, error } = execute(index, 'verify', ep);
557
- if (!ok) return toolError(error);
586
+ if (!ok) return te(error);
558
587
  return tr(output.formatVerify(result));
559
588
  }
560
589
 
561
590
  case 'plan': {
562
591
  index = getIndex(project_dir, ep);
563
592
  const { ok, result, error } = execute(index, 'plan', ep);
564
- if (!ok) return toolError(error);
593
+ if (!ok) return te(error);
565
594
  return tr(output.formatPlan(result));
566
595
  }
567
596
 
568
597
  case 'diff_impact': {
569
598
  index = getIndex(project_dir, ep);
570
599
  const { ok, result, error, note } = execute(index, 'diffImpact', ep);
571
- if (!ok) return toolError(error);
600
+ if (!ok) return te(error);
572
601
  let diText = output.formatDiffImpact(result, { all: ep.all });
573
602
  if (note) diText += '\n\n' + note;
574
603
  return tr(diText);
@@ -579,21 +608,21 @@ server.registerTool(
579
608
  case 'typedef': {
580
609
  index = getIndex(project_dir, ep);
581
610
  const { ok, result, error } = execute(index, 'typedef', ep);
582
- if (!ok) return toolError(error);
611
+ if (!ok) return te(error);
583
612
  return tr(output.formatTypedef(result, ep.name));
584
613
  }
585
614
 
586
615
  case 'stacktrace': {
587
616
  index = getIndex(project_dir, ep);
588
617
  const { ok, result, error } = execute(index, 'stacktrace', ep);
589
- if (!ok) return toolError(error);
618
+ if (!ok) return te(error);
590
619
  return tr(output.formatStackTrace(result));
591
620
  }
592
621
 
593
622
  case 'api': {
594
623
  index = getIndex(project_dir, ep);
595
624
  const { ok, result, error, note } = execute(index, 'api', ep);
596
- if (!ok) return toolError(error);
625
+ if (!ok) return te(error);
597
626
  let apiText = output.formatApi(result, ep.file || '.');
598
627
  if (note) apiText += '\n\n' + note;
599
628
  return tr(apiText);
@@ -602,7 +631,7 @@ server.registerTool(
602
631
  case 'stats': {
603
632
  index = getIndex(project_dir, ep);
604
633
  const { ok, result, error, note } = execute(index, 'stats', ep);
605
- if (!ok) return toolError(error);
634
+ if (!ok) return te(error);
606
635
  let statsText = output.formatStats(result, { top: ep.top || 0 });
607
636
  if (note) statsText += '\n\n' + note;
608
637
  return tr(statsText);
@@ -613,7 +642,7 @@ server.registerTool(
613
642
  case 'fn': {
614
643
  index = getIndex(project_dir, ep);
615
644
  const { ok, result, error, note } = execute(index, 'fn', ep);
616
- if (!ok) return toolError(error);
645
+ if (!ok) return te(error);
617
646
  // MCP path security: validate all result files are within project root
618
647
  for (const entry of result.entries) {
619
648
  const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
@@ -626,7 +655,7 @@ server.registerTool(
626
655
  case 'class': {
627
656
  index = getIndex(project_dir, ep);
628
657
  const { ok, result, error, note } = execute(index, 'class', ep);
629
- if (!ok) return toolError(error); // soft error (class not found)
658
+ if (!ok) return te(error); // soft error (class not found)
630
659
  // MCP path security: validate all result files are within project root
631
660
  for (const entry of result.entries) {
632
661
  const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
@@ -639,7 +668,7 @@ server.registerTool(
639
668
  case 'lines': {
640
669
  index = getIndex(project_dir, ep);
641
670
  const { ok, result, error } = execute(index, 'lines', ep);
642
- if (!ok) return toolError(error);
671
+ if (!ok) return te(error);
643
672
  // MCP path security: validate file is within project root
644
673
  const check = resolveAndValidatePath(index, result.relativePath);
645
674
  if (typeof check !== 'string') return check;
@@ -648,7 +677,7 @@ server.registerTool(
648
677
 
649
678
  case 'expand': {
650
679
  if (ep.item === undefined || ep.item === null) {
651
- return toolError('Item number is required (e.g. item=1).');
680
+ return te('Item number is required (e.g. item=1).');
652
681
  }
653
682
  index = getIndex(project_dir, ep);
654
683
  const lookup = expandCacheInstance.lookup(index.root, ep.item);
@@ -657,15 +686,15 @@ server.registerTool(
657
686
  itemCount: lookup.itemCount, symbolName: lookup.symbolName,
658
687
  validateRoot: true
659
688
  });
660
- if (!ok) return toolError(error);
689
+ if (!ok) return te(error);
661
690
  return tr(result.text);
662
691
  }
663
692
 
664
693
  default:
665
- return toolError(`Unknown command: ${command}`);
694
+ return te(`Unknown command: ${command}`);
666
695
  }
667
696
  } catch (e) {
668
- return toolError(e.message);
697
+ return te(e.message);
669
698
  } finally {
670
699
  // Persist calls cache after command execution.
671
700
  // getIndex() only saves after build (when callsCache is empty).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.8.18",
3
+ "version": "3.8.20",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
5
  "description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
6
6
  "main": "index.js",