ucn 3.8.18 → 3.8.19

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,41 @@ 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
+ // Core params that are always allowed (primary args, global options)
318
+ const coreParams = new Set([
319
+ 'name', 'term', 'stack', 'range', 'base', 'staged',
320
+ 'all', 'json', 'maxChars', 'maxFiles', 'workers',
321
+ ]);
322
+ for (const key of Object.keys(ep)) {
323
+ if (coreParams.has(key)) continue;
324
+ if (!applicable.includes(key) && ep[key] !== undefined &&
325
+ !(Array.isArray(ep[key]) && ep[key].length === 0)) {
326
+ strippedParams.push(REVERSE_PARAM_MAP[key] || key);
327
+ delete ep[key];
328
+ }
329
+ }
330
+ }
331
+
310
332
  // all=true bypasses both formatter caps AND char truncation (parity with CLI --all)
311
333
  const maxChars = ep.all ? MAX_OUTPUT_CHARS : ep.maxChars;
312
334
 
313
- // Wrap toolResult to auto-inject command + maxChars from this request
314
- const tr = (text) => toolResult(text, command, maxChars);
335
+ // Build stripping note (appended inside truncation boundary on success paths)
336
+ const strippedNote = strippedParams.length > 0
337
+ ? `\n\nNote: ${strippedParams.join(', ')} ignored (not applicable to ${command}).`
338
+ : '';
339
+
340
+ // Wrap toolResult to auto-inject command + maxChars + stripping note
341
+ const tr = (text) => toolResult(text, command, maxChars, strippedNote);
342
+ // Wrap toolError to include stripping note on error paths too
343
+ const te = strippedNote
344
+ ? (msg) => toolError(msg + strippedNote)
345
+ : toolError;
315
346
 
316
347
  let index = null; // Track for post-command cache save
317
348
  try {
@@ -326,7 +357,7 @@ server.registerTool(
326
357
  case 'about': {
327
358
  index = getIndex(project_dir, ep);
328
359
  const { ok, result, error, note } = execute(index, 'about', ep);
329
- if (!ok) return toolError(error);
360
+ if (!ok) return te(error);
330
361
  let aboutText = output.formatAbout(result, {
331
362
  allHint: 'Repeat with all=true to show all.',
332
363
  methodsHint: 'Note: obj.method() callers/callees excluded. Use include_methods=true to include them.',
@@ -339,7 +370,7 @@ server.registerTool(
339
370
  case 'context': {
340
371
  index = getIndex(project_dir, ep);
341
372
  const { ok, result: ctx, error, note } = execute(index, 'context', ep);
342
- if (!ok) return toolError(error);
373
+ if (!ok) return te(error);
343
374
  const { text, expandable } = output.formatContext(ctx, {
344
375
  expandHint: 'Use expand command with item number to see code for any item.',
345
376
  showConfidence: ep.showConfidence !== false,
@@ -353,7 +384,7 @@ server.registerTool(
353
384
  case 'impact': {
354
385
  index = getIndex(project_dir, ep);
355
386
  const { ok, result, error, note } = execute(index, 'impact', ep);
356
- if (!ok) return toolError(error);
387
+ if (!ok) return te(error);
357
388
  let impactText = output.formatImpact(result);
358
389
  if (note) impactText += '\n\n' + note;
359
390
  return tr(impactText);
@@ -362,7 +393,7 @@ server.registerTool(
362
393
  case 'blast': {
363
394
  index = getIndex(project_dir, ep);
364
395
  const { ok, result, error, note } = execute(index, 'blast', ep);
365
- if (!ok) return toolError(error);
396
+ if (!ok) return te(error);
366
397
  let blastText = output.formatBlast(result, {
367
398
  allHint: 'Set depth to expand all children.',
368
399
  });
@@ -373,7 +404,7 @@ server.registerTool(
373
404
  case 'smart': {
374
405
  index = getIndex(project_dir, ep);
375
406
  const { ok, result, error, note } = execute(index, 'smart', ep);
376
- if (!ok) return toolError(error);
407
+ if (!ok) return te(error);
377
408
  let smartText = output.formatSmart(result);
378
409
  if (note) smartText += '\n\n' + note;
379
410
  return tr(smartText);
@@ -382,7 +413,7 @@ server.registerTool(
382
413
  case 'trace': {
383
414
  index = getIndex(project_dir, ep);
384
415
  const { ok, result, error, note } = execute(index, 'trace', ep);
385
- if (!ok) return toolError(error);
416
+ if (!ok) return te(error);
386
417
  let traceText = output.formatTrace(result, {
387
418
  allHint: 'Set depth to expand all children.',
388
419
  methodsHint: 'Note: obj.method() calls excluded. Use include_methods=true to include them.'
@@ -394,7 +425,7 @@ server.registerTool(
394
425
  case 'reverse_trace': {
395
426
  index = getIndex(project_dir, ep);
396
427
  const { ok, result, error, note } = execute(index, 'reverseTrace', ep);
397
- if (!ok) return toolError(error);
428
+ if (!ok) return te(error);
398
429
  let rtText = output.formatReverseTrace(result, {
399
430
  allHint: 'Set depth to expand all children.',
400
431
  });
@@ -405,14 +436,14 @@ server.registerTool(
405
436
  case 'example': {
406
437
  index = getIndex(project_dir, ep);
407
438
  const { ok, result, error } = execute(index, 'example', ep);
408
- if (!ok) return toolError(error);
439
+ if (!ok) return te(error);
409
440
  return tr(output.formatExample(result, ep.name));
410
441
  }
411
442
 
412
443
  case 'related': {
413
444
  index = getIndex(project_dir, ep);
414
445
  const { ok, result, error, note } = execute(index, 'related', ep);
415
- if (!ok) return toolError(error);
446
+ if (!ok) return te(error);
416
447
  let relText = output.formatRelated(result, {
417
448
  all: ep.all || false, top: ep.top,
418
449
  allHint: 'Repeat with all=true to show all.'
@@ -426,7 +457,7 @@ server.registerTool(
426
457
  case 'find': {
427
458
  index = getIndex(project_dir, ep);
428
459
  const { ok, result, error, note } = execute(index, 'find', ep);
429
- if (!ok) return toolError(error);
460
+ if (!ok) return te(error);
430
461
  let text = output.formatFind(result, ep.name, ep.top);
431
462
  if (note) text += '\n\n' + note;
432
463
  return tr(text);
@@ -435,7 +466,7 @@ server.registerTool(
435
466
  case 'usages': {
436
467
  index = getIndex(project_dir, ep);
437
468
  const { ok, result, error, note } = execute(index, 'usages', ep);
438
- if (!ok) return toolError(error);
469
+ if (!ok) return te(error);
439
470
  let text = output.formatUsages(result, ep.name);
440
471
  if (note) text += '\n\n' + note;
441
472
  return tr(text);
@@ -444,7 +475,7 @@ server.registerTool(
444
475
  case 'toc': {
445
476
  index = getIndex(project_dir, ep);
446
477
  const { ok, result, error, note } = execute(index, 'toc', ep);
447
- if (!ok) return toolError(error);
478
+ if (!ok) return te(error);
448
479
  let text = output.formatToc(result, {
449
480
  topHint: 'Set top=N or use detailed=false for compact view.'
450
481
  });
@@ -455,7 +486,7 @@ server.registerTool(
455
486
  case 'search': {
456
487
  index = getIndex(project_dir, ep);
457
488
  const { ok, result, error, structural, note } = execute(index, 'search', ep);
458
- if (!ok) return toolError(error);
489
+ if (!ok) return te(error);
459
490
  let searchText;
460
491
  if (structural) {
461
492
  searchText = output.formatStructuralSearch(result);
@@ -469,7 +500,7 @@ server.registerTool(
469
500
  case 'tests': {
470
501
  index = getIndex(project_dir, ep);
471
502
  const { ok, result, error, note } = execute(index, 'tests', ep);
472
- if (!ok) return toolError(error);
503
+ if (!ok) return te(error);
473
504
  let testsText = output.formatTests(result, ep.name);
474
505
  if (note) testsText += '\n\n' + note;
475
506
  return tr(testsText);
@@ -478,7 +509,7 @@ server.registerTool(
478
509
  case 'affected_tests': {
479
510
  index = getIndex(project_dir, ep);
480
511
  const { ok, result, error, note } = execute(index, 'affectedTests', ep);
481
- if (!ok) return toolError(error);
512
+ if (!ok) return te(error);
482
513
  let atText = output.formatAffectedTests(result, { all: ep.all });
483
514
  if (note) atText += '\n\n' + note;
484
515
  return tr(atText);
@@ -487,7 +518,7 @@ server.registerTool(
487
518
  case 'deadcode': {
488
519
  index = getIndex(project_dir, ep);
489
520
  const { ok, result, error, note } = execute(index, 'deadcode', ep);
490
- if (!ok) return toolError(error);
521
+ if (!ok) return te(error);
491
522
  const dcNote = note;
492
523
  let dcText = output.formatDeadcode(result, {
493
524
  top: ep.top || 0,
@@ -501,7 +532,7 @@ server.registerTool(
501
532
  case 'entrypoints': {
502
533
  index = getIndex(project_dir, ep);
503
534
  const { ok, result, error, note } = execute(index, 'entrypoints', ep);
504
- if (!ok) return toolError(error);
535
+ if (!ok) return te(error);
505
536
  let epText = output.formatEntrypoints(result);
506
537
  if (note) epText += '\n\n' + note;
507
538
  return tr(epText);
@@ -512,28 +543,28 @@ server.registerTool(
512
543
  case 'imports': {
513
544
  index = getIndex(project_dir, ep);
514
545
  const { ok, result, error } = execute(index, 'imports', ep);
515
- if (!ok) return toolError(error);
546
+ if (!ok) return te(error);
516
547
  return tr(output.formatImports(result, ep.file));
517
548
  }
518
549
 
519
550
  case 'exporters': {
520
551
  index = getIndex(project_dir, ep);
521
552
  const { ok, result, error } = execute(index, 'exporters', ep);
522
- if (!ok) return toolError(error);
553
+ if (!ok) return te(error);
523
554
  return tr(output.formatExporters(result, ep.file));
524
555
  }
525
556
 
526
557
  case 'file_exports': {
527
558
  index = getIndex(project_dir, ep);
528
559
  const { ok, result, error } = execute(index, 'fileExports', ep);
529
- if (!ok) return toolError(error);
560
+ if (!ok) return te(error);
530
561
  return tr(output.formatFileExports(result, ep.file));
531
562
  }
532
563
 
533
564
  case 'graph': {
534
565
  index = getIndex(project_dir, ep);
535
566
  const { ok, result, error } = execute(index, 'graph', ep);
536
- if (!ok) return toolError(error);
567
+ if (!ok) return te(error);
537
568
  return tr(output.formatGraph(result, {
538
569
  showAll: ep.all || ep.depth !== undefined,
539
570
  maxDepth: ep.depth ?? 2, file: ep.file,
@@ -545,7 +576,7 @@ server.registerTool(
545
576
  case 'circular_deps': {
546
577
  index = getIndex(project_dir, ep);
547
578
  const { ok, result, error } = execute(index, 'circularDeps', ep);
548
- if (!ok) return toolError(error);
579
+ if (!ok) return te(error);
549
580
  return tr(output.formatCircularDeps(result));
550
581
  }
551
582
 
@@ -554,21 +585,21 @@ server.registerTool(
554
585
  case 'verify': {
555
586
  index = getIndex(project_dir, ep);
556
587
  const { ok, result, error } = execute(index, 'verify', ep);
557
- if (!ok) return toolError(error);
588
+ if (!ok) return te(error);
558
589
  return tr(output.formatVerify(result));
559
590
  }
560
591
 
561
592
  case 'plan': {
562
593
  index = getIndex(project_dir, ep);
563
594
  const { ok, result, error } = execute(index, 'plan', ep);
564
- if (!ok) return toolError(error);
595
+ if (!ok) return te(error);
565
596
  return tr(output.formatPlan(result));
566
597
  }
567
598
 
568
599
  case 'diff_impact': {
569
600
  index = getIndex(project_dir, ep);
570
601
  const { ok, result, error, note } = execute(index, 'diffImpact', ep);
571
- if (!ok) return toolError(error);
602
+ if (!ok) return te(error);
572
603
  let diText = output.formatDiffImpact(result, { all: ep.all });
573
604
  if (note) diText += '\n\n' + note;
574
605
  return tr(diText);
@@ -579,21 +610,21 @@ server.registerTool(
579
610
  case 'typedef': {
580
611
  index = getIndex(project_dir, ep);
581
612
  const { ok, result, error } = execute(index, 'typedef', ep);
582
- if (!ok) return toolError(error);
613
+ if (!ok) return te(error);
583
614
  return tr(output.formatTypedef(result, ep.name));
584
615
  }
585
616
 
586
617
  case 'stacktrace': {
587
618
  index = getIndex(project_dir, ep);
588
619
  const { ok, result, error } = execute(index, 'stacktrace', ep);
589
- if (!ok) return toolError(error);
620
+ if (!ok) return te(error);
590
621
  return tr(output.formatStackTrace(result));
591
622
  }
592
623
 
593
624
  case 'api': {
594
625
  index = getIndex(project_dir, ep);
595
626
  const { ok, result, error, note } = execute(index, 'api', ep);
596
- if (!ok) return toolError(error);
627
+ if (!ok) return te(error);
597
628
  let apiText = output.formatApi(result, ep.file || '.');
598
629
  if (note) apiText += '\n\n' + note;
599
630
  return tr(apiText);
@@ -602,7 +633,7 @@ server.registerTool(
602
633
  case 'stats': {
603
634
  index = getIndex(project_dir, ep);
604
635
  const { ok, result, error, note } = execute(index, 'stats', ep);
605
- if (!ok) return toolError(error);
636
+ if (!ok) return te(error);
606
637
  let statsText = output.formatStats(result, { top: ep.top || 0 });
607
638
  if (note) statsText += '\n\n' + note;
608
639
  return tr(statsText);
@@ -613,7 +644,7 @@ server.registerTool(
613
644
  case 'fn': {
614
645
  index = getIndex(project_dir, ep);
615
646
  const { ok, result, error, note } = execute(index, 'fn', ep);
616
- if (!ok) return toolError(error);
647
+ if (!ok) return te(error);
617
648
  // MCP path security: validate all result files are within project root
618
649
  for (const entry of result.entries) {
619
650
  const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
@@ -626,7 +657,7 @@ server.registerTool(
626
657
  case 'class': {
627
658
  index = getIndex(project_dir, ep);
628
659
  const { ok, result, error, note } = execute(index, 'class', ep);
629
- if (!ok) return toolError(error); // soft error (class not found)
660
+ if (!ok) return te(error); // soft error (class not found)
630
661
  // MCP path security: validate all result files are within project root
631
662
  for (const entry of result.entries) {
632
663
  const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
@@ -639,7 +670,7 @@ server.registerTool(
639
670
  case 'lines': {
640
671
  index = getIndex(project_dir, ep);
641
672
  const { ok, result, error } = execute(index, 'lines', ep);
642
- if (!ok) return toolError(error);
673
+ if (!ok) return te(error);
643
674
  // MCP path security: validate file is within project root
644
675
  const check = resolveAndValidatePath(index, result.relativePath);
645
676
  if (typeof check !== 'string') return check;
@@ -648,7 +679,7 @@ server.registerTool(
648
679
 
649
680
  case 'expand': {
650
681
  if (ep.item === undefined || ep.item === null) {
651
- return toolError('Item number is required (e.g. item=1).');
682
+ return te('Item number is required (e.g. item=1).');
652
683
  }
653
684
  index = getIndex(project_dir, ep);
654
685
  const lookup = expandCacheInstance.lookup(index.root, ep.item);
@@ -657,15 +688,15 @@ server.registerTool(
657
688
  itemCount: lookup.itemCount, symbolName: lookup.symbolName,
658
689
  validateRoot: true
659
690
  });
660
- if (!ok) return toolError(error);
691
+ if (!ok) return te(error);
661
692
  return tr(result.text);
662
693
  }
663
694
 
664
695
  default:
665
- return toolError(`Unknown command: ${command}`);
696
+ return te(`Unknown command: ${command}`);
666
697
  }
667
698
  } catch (e) {
668
- return toolError(e.message);
699
+ return te(e.message);
669
700
  } finally {
670
701
  // Persist calls cache after command execution.
671
702
  // 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.19",
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",