ucn 5.1.1 → 5.2.1

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/languages/rust.js CHANGED
@@ -444,6 +444,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
444
444
  const iteratorItemType = extractRustIteratorItemType(node);
445
445
  const docstring = extractRustDocstring(lines, startLine);
446
446
  const generics = extractGenerics(node);
447
+ const genericBounds = extractGenericBounds(node);
447
448
  const attributes = extractAttributes(node, lines);
448
449
  const attributesWithArgs = extractAttributesWithArgs(node, lines);
449
450
  const inCfgTest = _isInsideCfgTestModule(node, lines);
@@ -476,6 +477,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
476
477
  ...(iteratorItemType && { iteratorItemType }),
477
478
  ...(docstring && { docstring }),
478
479
  ...(generics && { generics }),
480
+ ...(genericBounds && { genericBounds }),
479
481
  ...(attributesWithArgs.length > 0 && { attributesWithArgs })
480
482
  });
481
483
  }
@@ -1029,6 +1031,39 @@ function extractGenerics(node) {
1029
1031
  return null;
1030
1032
  }
1031
1033
 
1034
+ /**
1035
+ * Compiler-declared Rust type-parameter bounds from both `<T: Trait>` and
1036
+ * `where T: Trait`. Keep only nominal trait heads from AST type nodes;
1037
+ * lifetimes and unparseable shapes add no evidence.
1038
+ */
1039
+ function extractGenericBounds(node) {
1040
+ const result = new Map();
1041
+ const record = declaration => {
1042
+ if (!declaration) return;
1043
+ const children = declaration.namedChildren || [];
1044
+ const parameter = children.find(child => child.type === 'type_identifier');
1045
+ const bounds = children.find(child => child.type === 'trait_bounds');
1046
+ if (!parameter || !bounds) return;
1047
+ const names = bounds.namedChildren
1048
+ .map(bound => aliasBaseTypeName(bound))
1049
+ .filter(Boolean);
1050
+ if (names.length === 0) return;
1051
+ if (!result.has(parameter.text)) result.set(parameter.text, new Set());
1052
+ for (const name of names) result.get(parameter.text).add(name);
1053
+ };
1054
+ const typeParameters = node.childForFieldName('type_parameters');
1055
+ for (const child of typeParameters?.namedChildren || []) {
1056
+ if (child.type === 'constrained_type_parameter') record(child);
1057
+ }
1058
+ const whereClause = node.namedChildren.find(child => child.type === 'where_clause');
1059
+ for (const child of whereClause?.namedChildren || []) {
1060
+ if (child.type === 'where_predicate') record(child);
1061
+ }
1062
+ if (result.size === 0) return null;
1063
+ return Object.fromEntries([...result].map(([name, bounds]) =>
1064
+ [name, [...bounds].sort()]));
1065
+ }
1066
+
1032
1067
  /**
1033
1068
  * Find all types (structs, enums, traits, impls) in Rust code
1034
1069
  */
@@ -1317,6 +1352,7 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
1317
1352
  if (inCfgTest) modifiers.push('cfg_test_module');
1318
1353
 
1319
1354
  const memberGenerics = extractGenerics(child);
1355
+ const genericBounds = extractGenericBounds(child);
1320
1356
  const callbackParamTypes = extractRustCallbackParamTypes(paramsNode);
1321
1357
  members.push({
1322
1358
  name: nameNode.text,
@@ -1335,7 +1371,8 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
1335
1371
  ...(docstring && { docstring }),
1336
1372
  // Method-level type params (fix #229): generic-param receiver
1337
1373
  // types inside the method resolve against this declaration.
1338
- ...(memberGenerics && { generics: memberGenerics })
1374
+ ...(memberGenerics && { generics: memberGenerics }),
1375
+ ...(genericBounds && { genericBounds })
1339
1376
  });
1340
1377
  }
1341
1378
  }
@@ -1491,8 +1528,48 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1491
1528
  isPatternShadow, isFlowInvalidated, context = 'invocation') {
1492
1529
  const contextKind = typeof context === 'string' ? context : context.kind;
1493
1530
  const containerMacro = typeof context === 'object' ? context.containerMacro : undefined;
1531
+ const inheritedTokenTypes = typeof context === 'object' && context.tokenTypes
1532
+ ? context.tokenTypes : new Map();
1494
1533
  const children = [];
1495
1534
  for (let i = 0; i < tree.childCount; i++) children.push(tree.child(i));
1535
+ // Macro arguments are token trees, so a typed closure parameter is not a
1536
+ // normal closure_parameters AST node. Recover only the compiler-explicit
1537
+ // simple/path type shape from AST tokens and pass it into that closure's
1538
+ // body token tree. This keeps the evidence lexical: sibling macro
1539
+ // arguments and token trees before the closure never inherit the name.
1540
+ const closureBodyTypes = new Map();
1541
+ for (let i = 0; i < children.length; i++) {
1542
+ if (children[i].type !== '|') continue;
1543
+ let close = i + 1;
1544
+ while (close < children.length && children[close].type !== '|') close++;
1545
+ if (close >= children.length) break;
1546
+ const bindings = new Map(inheritedTokenTypes);
1547
+ let cursor = i + 1;
1548
+ while (cursor < close) {
1549
+ const nameNode = children[cursor];
1550
+ if (nameNode?.type !== 'identifier' || children[cursor + 1]?.type !== ':') {
1551
+ cursor++;
1552
+ continue;
1553
+ }
1554
+ let end = cursor + 2;
1555
+ while (end < close && children[end].type !== ',') end++;
1556
+ const typeTokens = children.slice(cursor + 2, end).filter(token =>
1557
+ !['&', 'mutable_specifier', 'lifetime'].includes(token.type));
1558
+ const simplePath = typeTokens.length > 0 && typeTokens.every((token, index) =>
1559
+ token.type === 'identifier' ||
1560
+ (token.type === '::' && index > 0 && index < typeTokens.length - 1));
1561
+ if (simplePath) {
1562
+ const identifiers = typeTokens.filter(token => token.type === 'identifier');
1563
+ if (identifiers.length > 0) {
1564
+ bindings.set(nameNode.text, identifiers[identifiers.length - 1].text);
1565
+ }
1566
+ }
1567
+ cursor = end + 1;
1568
+ }
1569
+ const body = children[close + 1];
1570
+ if (body?.type === 'token_tree') closureBodyTypes.set(body.id, bindings);
1571
+ i = close;
1572
+ }
1496
1573
  let lastProducer = null;
1497
1574
  const macroFields = {
1498
1575
  inMacro: true,
@@ -1502,8 +1579,13 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1502
1579
  for (let i = 0; i < children.length; i++) {
1503
1580
  const tok = children[i];
1504
1581
  if (tok.type === 'token_tree') {
1582
+ const tokenTypes = closureBodyTypes.get(tok.id) || inheritedTokenTypes;
1505
1583
  extractCallsFromTokenTree(tok, enclosingFunction, calls, getReceiverType,
1506
- isPatternShadow, isFlowInvalidated, context);
1584
+ isPatternShadow, isFlowInvalidated, {
1585
+ kind: contextKind,
1586
+ ...(containerMacro && { containerMacro }),
1587
+ tokenTypes,
1588
+ });
1507
1589
  continue;
1508
1590
  }
1509
1591
  // `default` is tokenized as the Rust keyword even in the valid
@@ -1620,8 +1702,9 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
1620
1702
  ? ({ string_literal: 'str', raw_string_literal: 'str',
1621
1703
  char_literal: 'char', boolean_literal: 'bool' })[recvTok.type]
1622
1704
  : undefined;
1623
- const receiverType = (receiver && receiver !== 'self' && getReceiverType)
1624
- ? getReceiverType(receiver, tok) : litType;
1705
+ const receiverType = (receiver && receiver !== 'self')
1706
+ ? (getReceiverType?.(receiver, tok) || inheritedTokenTypes.get(receiver))
1707
+ : litType;
1625
1708
  const receiverPatternShadow = !!(receiver && isPatternShadow?.(tok, receiver));
1626
1709
  const receiverFlowInvalidated = !!(receiver && isFlowInvalidated?.(tok, receiver));
1627
1710
  const iterationSource = rustIterationSourceOf(tok, receiver);
@@ -242,6 +242,17 @@ function parseGoParam(param, info) {
242
242
  }
243
243
  if (names.length > 0) info.name = names[0];
244
244
  if (typeNode) info.type = typeNode.text;
245
+ // Interface method declarations commonly omit parameter names:
246
+ // `Match(*http.Request, *RouteMatch) bool`. These are still two real
247
+ // signature slots. Dropping them made verify/plan see zero arguments
248
+ // and prevented interface-dispatch call sites from joining a rename
249
+ // slot. Preserve the authored type as the display token while marking
250
+ // it unnamed so signature consumers can distinguish it from a name.
251
+ if (names.length === 0 && typeNode) {
252
+ info.name = typeNode.text;
253
+ info.unnamed = true;
254
+ delete info.type;
255
+ }
245
256
  // Store additional names for multi-param declarations (handled by parseStructuredParams)
246
257
  if (names.length > 1) {
247
258
  info._additionalNames = names.slice(1);
package/mcp/server.js CHANGED
@@ -9,22 +9,7 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
-
13
- // ============================================================================
14
- // MCP SDK IMPORTS (dynamic, to handle missing dependency gracefully)
15
- // ============================================================================
16
-
17
- let McpServer, StdioServerTransport, z;
18
-
19
- try {
20
- ({ McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js'));
21
- ({ StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'));
22
- z = require('zod');
23
- } catch (e) {
24
- console.error('Missing dependencies. Install with:');
25
- console.error(' npm install @modelcontextprotocol/sdk zod');
26
- process.exit(1);
27
- }
12
+ const { StdioMcpServer } = require('./stdio-server');
28
13
 
29
14
  // ============================================================================
30
15
  // UCN CORE IMPORTS
@@ -116,7 +101,7 @@ function getIndex(projectDir, options) {
116
101
  // SERVER SETUP
117
102
  // ============================================================================
118
103
 
119
- const server = new McpServer({
104
+ const server = new StdioMcpServer({
120
105
  name: 'ucn',
121
106
  version: require('../package.json').version
122
107
  });
@@ -249,97 +234,109 @@ CONFIRMED carries target-identity evidence. UNVERIFIED is possible and requires
249
234
  // and made the single MCP tool difficult for agents to parse.
250
235
  const TOOL_DESCRIPTION = CONCISE_TOOL_DESCRIPTION;
251
236
 
252
- // The schema shape is named so the handler can distinguish known keys from
253
- // unknown/typo'd/camelCase ones — z.object() would otherwise strip them
254
- // silently, and a silently ignored parameter changes the answer with no
255
- // signal (e.g. include_test:true returning untested-filtered results).
237
+ const stringParam = (description, extra = {}) => ({ type: 'string', description, ...extra });
238
+ const booleanParam = description => ({ type: 'boolean', description });
239
+ const integerParam = (description, extra = {}) => ({ type: 'integer', description, ...extra });
240
+ const numberParam = (description, extra = {}) => ({ type: 'number', description, ...extra });
241
+
242
+ // Keep the public JSON Schema and runtime validation rules in one dependency-
243
+ // free object. Unknown keys remain allowed so the handler can return typo and
244
+ // applicability guidance instead of silently changing the requested answer.
256
245
  const INPUT_SHAPE = {
257
- command: null, // assigned below (needs z at load time)
246
+ command: stringParam(
247
+ `UCN task command. One of: ${getMcpCommandEnum().join(', ')}.`,
248
+ { enum: getMcpCommandEnum() },
249
+ ),
250
+ project_dir: stringParam('Non-empty absolute path, or a path relative to the MCP server process working directory, identifying the project to analyze.', { minLength: 1 }),
251
+ name: stringParam('Symbol name or stable path:line:name handle. Used by show/find/usages/source/trace/impact/tests/check/plan.'),
252
+ file: stringParam('File target for source/deps/api or a symbol-disambiguation filter.'),
253
+ sections: stringParam('Comma-separated projection. show: summary,callers,callees,source,dependencies,tests,types,example,related. repo: summary,files,stats,health.'),
254
+ exclude: stringParam('Comma-separated patterns to exclude (e.g. "test,mock,vendor")'),
255
+ include_tests: booleanParam('Include test files in results (excluded by default)'),
256
+ exclude_tests: booleanParam('Explicit spelling of the default test-file exclusion (entrypoints). Use include_tests=true to include test files.'),
257
+ include_methods: booleanParam('Include method callees where receiver evidence permits. Caller-bearing views always tier method sites.'),
258
+ expand_unverified: booleanParam('trace callers: follow unverified edges; downstream nodes remain marked possible, never confirmed.'),
259
+ min_confidence: numberParam('Minimum ordinal evidence weight (legacy name; not a probability) for caller/callee edges', { minimum: 0, maximum: 1 }),
260
+ show_confidence: booleanParam('show: resolution-evidence labels default to visible; set false to hide them. Numeric weights are ordinal, not probabilities.'),
261
+ unreachable_only: booleanParam('show/impact: retain only relationships unreachable from detected entry points.'),
262
+ with_types: booleanParam('show: include related type definitions.'),
263
+ with_source: booleanParam('Attach exact source to find results.'),
264
+ detailed: booleanParam('repo files: show symbols per file; deps: include import declarations and importers.'),
265
+ exact: booleanParam('Exact name match only (no substring matching)'),
266
+ in: stringParam('Only search in this directory path (e.g. "src/core")'),
267
+ top: integerParam('Max results to show (default: 10). Must be a positive integer.', { exclusiveMinimum: 0, maximum: 10000 }),
268
+ depth: integerParam('Max depth (default: 3 for trace, 2 for deps); expands all children. Non-negative integer.', { minimum: 0, maximum: 100 }),
269
+ code_only: booleanParam('Exclude matches in comments and strings'),
270
+ context: integerParam('Lines of context around each match. Non-negative integer.', { minimum: 0, maximum: 1000 }),
271
+ include_exported: booleanParam('Include exported symbols in deadcode results'),
272
+ include_decorated: booleanParam('Include decorated/annotated symbols in deadcode results'),
273
+ calls_only: booleanParam('tests: retain direct calls and test-case matches only.'),
274
+ max_lines: integerParam('source: maximum lines for large class-like declarations.', { exclusiveMinimum: 0, maximum: 1000000 }),
275
+ direction: stringParam('trace: callees/callers. deps: imports/importers/both.', { enum: ['callees', 'callers', 'imports', 'importers', 'both'] }),
276
+ to: stringParam('trace with direction=callers: continue toward entry points.', { enum: ['entrypoints'] }),
277
+ cycles: booleanParam('deps: report circular imports instead of a file graph.'),
278
+ term: stringParam('Literal search term by default. Set regex=true only for regular-expression syntax.'),
279
+ regex: booleanParam('Treat search term as a regular expression (default: false/literal). Ordinary patterns run in an RE2-compatible linear-time engine; unsafe nested repetition is rejected.'),
280
+ functions: booleanParam('repo stats: include per-function line counts sorted by size.'),
281
+ hot: booleanParam('repo stats: include the top N most-called functions.'),
282
+ diverse: booleanParam('show example: return representatives from distinct argument shapes.'),
283
+ git: booleanParam('show summary: attach last-modified, author, and recent-change metadata.'),
284
+ add_param: stringParam('Parameter name to add (plan command)'),
285
+ remove_param: stringParam('Parameter name to remove (plan command)'),
286
+ rename_to: stringParam('New function name (plan command)'),
287
+ default_value: stringParam('Default value for added parameter (plan command)'),
288
+ stack: stringParam('The stack trace text to parse (stacktrace command)'),
289
+ range: stringParam('source line range, e.g. "10-20" or "15"; requires file.'),
290
+ base: stringParam('Git ref to diff against (default: HEAD). E.g. "HEAD~3", "main", a commit SHA'),
291
+ staged: booleanParam('impact/check without a symbol: analyze staged changes.'),
292
+ deep: booleanParam('repo: include health and sample the ordinal resolution-evidence profile, not accuracy.'),
293
+ compact: booleanParam('Compact output defaults to true for show/impact and false for usages; set the opposite value to change that command\'s presentation.'),
294
+ case_sensitive: booleanParam('Case-sensitive search (default: false, case-insensitive)'),
295
+ all: booleanParam('Lift formatter and result caps where supported.'),
296
+ top_level: booleanParam('repo files: show only top-level functions.'),
297
+ class_name: stringParam('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)'),
298
+ line: integerParam('Definition line pin. Resolves the symbol defined at this exact line (the middle component of a file:line:name handle). Disambiguates same-file same-name definitions.', { exclusiveMinimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
299
+ limit: integerParam('Max results to return (default: 500). Caps find, usages, search, deadcode, api, and repo files. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 1000000 }),
300
+ max_files: integerParam('Max files to index (default: 10000). Use for very large codebases. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 10000000 }),
301
+ max_chars: integerParam('Max output chars before truncation. Broad sweep commands (repo, entrypoints, endpoints, deadcode, deps, check, audit_async) default to 3K; all other commands default to 10K. Maximum: 100K. all=true lifts formatter caps but keeps the 100K transport ceiling.', { exclusiveMinimum: 0, maximum: 100000 }),
302
+ type: stringParam('Symbol type filter for structural search: function, class, call, method, type, state, field, constant, macro. Triggers index-based search.'),
303
+ param: stringParam('Filter by parameter name or type (structural search). E.g. "Request", "ctx".'),
304
+ receiver: stringParam('Filter calls by receiver (structural search, type=call). E.g. "db", "http".'),
305
+ returns: stringParam('Filter by return type (structural search). E.g. "Promise", "error".'),
306
+ decorator: stringParam('Filter by decorator/annotation (structural search). E.g. "Route", "Test".'),
307
+ exported: booleanParam('Only exported/public symbols (structural search).'),
308
+ unused: booleanParam('Only symbols with zero callers (structural search).'),
309
+ framework: stringParam('Filter entrypoints by framework (e.g. "express", "spring", "flask"). Comma-separated for multiple.'),
310
+ follow_symlinks: booleanParam('Follow symlinks during file discovery (default: true)'),
311
+ bridge: booleanParam('Match server routes to client requests (endpoints command).'),
312
+ server_only: booleanParam('Only list server routes (endpoints command).'),
313
+ client_only: booleanParam('Only list client requests (endpoints command).'),
314
+ unmatched: booleanParam('Only show unmatched routes/requests (endpoints command).'),
315
+ method: stringParam('Filter by HTTP method (e.g. "GET", "POST") for endpoints.'),
316
+ prefix: stringParam('Filter routes/requests by path prefix (endpoints command).'),
317
+ hide_uncertain: booleanParam('Hide uncertain (interpolated-path) bridges (endpoints command).'),
318
+ };
319
+
320
+ const INPUT_SCHEMA = {
321
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
322
+ type: 'object',
323
+ properties: INPUT_SHAPE,
324
+ required: ['command', 'project_dir'],
325
+ additionalProperties: {},
258
326
  };
259
327
 
260
328
  server.registerTool(
261
329
  'ucn',
262
330
  {
263
331
  description: TOOL_DESCRIPTION,
264
- inputSchema: z.object(Object.assign(INPUT_SHAPE, {
265
- // Runtime validation stays string-based so retired v4 names reach
266
- // directive migration guidance. Zod metadata publishes the strict
267
- // v5 enum to MCP clients without making retired names aliases.
268
- command: z.string()
269
- .meta({ enum: getMcpCommandEnum() })
270
- .describe(`UCN task command. One of: ${getMcpCommandEnum().join(', ')}.`),
271
- project_dir: z.string().trim().min(1, 'project_dir is required and must be a non-empty path.').describe('Non-empty absolute path, or a path relative to the MCP server process working directory, identifying the project to analyze.'),
272
- name: z.string().optional().describe('Symbol name or stable path:line:name handle. Used by show/find/usages/source/trace/impact/tests/check/plan.'),
273
- file: z.string().optional().describe('File target for source/deps/api or a symbol-disambiguation filter.'),
274
- sections: z.string().optional().describe('Comma-separated projection. show: summary,callers,callees,source,dependencies,tests,types,example,related. repo: summary,files,stats,health.'),
275
- exclude: z.string().optional().describe('Comma-separated patterns to exclude (e.g. "test,mock,vendor")'),
276
- include_tests: z.boolean().optional().describe('Include test files in results (excluded by default)'),
277
- exclude_tests: z.boolean().optional().describe('Explicit spelling of the default test-file exclusion (entrypoints). Use include_tests=true to include test files.'),
278
- include_methods: z.boolean().optional().describe('Include method callees where receiver evidence permits. Caller-bearing views always tier method sites.'),
279
- expand_unverified: z.boolean().optional().describe('trace callers: follow unverified edges; downstream nodes remain marked possible, never confirmed.'),
280
- min_confidence: z.number().min(0).max(1).optional().describe('Minimum ordinal evidence weight (legacy name; not a probability) for caller/callee edges'),
281
- show_confidence: z.boolean().optional().describe('show: resolution-evidence labels default to visible; set false to hide them. Numeric weights are ordinal, not probabilities.'),
282
- unreachable_only: z.boolean().optional().describe('show/impact: retain only relationships unreachable from detected entry points.'),
283
- with_types: z.boolean().optional().describe('show: include related type definitions.'),
284
- with_source: z.boolean().optional().describe('Attach exact source to find results.'),
285
- detailed: z.boolean().optional().describe('repo files: show symbols per file; deps: include import declarations and importers.'),
286
- exact: z.boolean().optional().describe('Exact name match only (no substring matching)'),
287
- in: z.string().optional().describe('Only search in this directory path (e.g. "src/core")'),
288
- top: z.number().int().positive().max(10000).optional().describe('Max results to show (default: 10). Must be a positive integer.'),
289
- depth: z.number().int().nonnegative().max(100).optional().describe('Max depth (default: 3 for trace, 2 for deps); expands all children. Non-negative integer.'),
290
- code_only: z.boolean().optional().describe('Exclude matches in comments and strings'),
291
- context: z.number().int().nonnegative().max(1000).optional().describe('Lines of context around each match. Non-negative integer.'),
292
- include_exported: z.boolean().optional().describe('Include exported symbols in deadcode results'),
293
- include_decorated: z.boolean().optional().describe('Include decorated/annotated symbols in deadcode results'),
294
- calls_only: z.boolean().optional().describe('tests: retain direct calls and test-case matches only.'),
295
- max_lines: z.number().int().positive().max(1000000).optional().describe('source: maximum lines for large class-like declarations.'),
296
- direction: z.enum(['callees', 'callers', 'imports', 'importers', 'both']).optional().describe('trace: callees/callers. deps: imports/importers/both.'),
297
- to: z.enum(['entrypoints']).optional().describe('trace with direction=callers: continue toward entry points.'),
298
- cycles: z.boolean().optional().describe('deps: report circular imports instead of a file graph.'),
299
- term: z.string().optional().describe('Literal search term by default. Set regex=true only for regular-expression syntax.'),
300
- regex: z.boolean().optional().describe('Treat search term as a regular expression (default: false/literal). Ordinary patterns run in an RE2-compatible linear-time engine; unsafe nested repetition is rejected.'),
301
- functions: z.boolean().optional().describe('repo stats: include per-function line counts sorted by size.'),
302
- hot: z.boolean().optional().describe('repo stats: include the top N most-called functions.'),
303
- diverse: z.boolean().optional().describe('show example: return representatives from distinct argument shapes.'),
304
- git: z.boolean().optional().describe('show summary: attach last-modified, author, and recent-change metadata.'),
305
- add_param: z.string().optional().describe('Parameter name to add (plan command)'),
306
- remove_param: z.string().optional().describe('Parameter name to remove (plan command)'),
307
- rename_to: z.string().optional().describe('New function name (plan command)'),
308
- default_value: z.string().optional().describe('Default value for added parameter (plan command)'),
309
- stack: z.string().optional().describe('The stack trace text to parse (stacktrace command)'),
310
- range: z.string().optional().describe('source line range, e.g. "10-20" or "15"; requires file.'),
311
- base: z.string().optional().describe('Git ref to diff against (default: HEAD). E.g. "HEAD~3", "main", a commit SHA'),
312
- staged: z.boolean().optional().describe('impact/check without a symbol: analyze staged changes.'),
313
- deep: z.boolean().optional().describe('repo: include health and sample the ordinal resolution-evidence profile, not accuracy.'),
314
- compact: z.boolean().optional().describe('Compact output defaults to true for show/impact and false for usages; set the opposite value to change that command\'s presentation.'),
315
- case_sensitive: z.boolean().optional().describe('Case-sensitive search (default: false, case-insensitive)'),
316
- all: z.boolean().optional().describe('Lift formatter and result caps where supported.'),
317
- top_level: z.boolean().optional().describe('repo files: show only top-level functions.'),
318
- class_name: z.string().optional().describe('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)'),
319
- line: z.number().int().positive().optional().describe('Definition line pin. Resolves the symbol defined at this exact line (the middle component of a file:line:name handle). Disambiguates same-file same-name definitions.'),
320
- limit: z.number().int().positive().max(1000000).optional().describe('Max results to return (default: 500). Caps find, usages, search, deadcode, api, and repo files. Must be a positive integer.'),
321
- max_files: z.number().int().positive().max(10000000).optional().describe('Max files to index (default: 10000). Use for very large codebases. Must be a positive integer.'),
322
- max_chars: z.number().int().positive().max(100000).optional().describe('Max output chars before truncation. Broad sweep commands (repo, entrypoints, endpoints, deadcode, deps, check, audit_async) default to 3K; all other commands default to 10K. Maximum: 100K. all=true lifts formatter caps but keeps the 100K transport ceiling.'),
323
- // Structural search flags (search command)
324
- type: z.string().optional().describe('Symbol type filter for structural search: function, class, call, method, type, state, field, constant, macro. Triggers index-based search.'),
325
- param: z.string().optional().describe('Filter by parameter name or type (structural search). E.g. "Request", "ctx".'),
326
- receiver: z.string().optional().describe('Filter calls by receiver (structural search, type=call). E.g. "db", "http".'),
327
- returns: z.string().optional().describe('Filter by return type (structural search). E.g. "Promise", "error".'),
328
- decorator: z.string().optional().describe('Filter by decorator/annotation (structural search). E.g. "Route", "Test".'),
329
- exported: z.boolean().optional().describe('Only exported/public symbols (structural search).'),
330
- unused: z.boolean().optional().describe('Only symbols with zero callers (structural search).'),
331
- framework: z.string().optional().describe('Filter entrypoints by framework (e.g. "express", "spring", "flask"). Comma-separated for multiple.'),
332
- follow_symlinks: z.boolean().optional().describe('Follow symlinks during file discovery (default: true)'),
333
- // endpoints command
334
- bridge: z.boolean().optional().describe('Match server routes to client requests (endpoints command).'),
335
- server_only: z.boolean().optional().describe('Only list server routes (endpoints command).'),
336
- client_only: z.boolean().optional().describe('Only list client requests (endpoints command).'),
337
- unmatched: z.boolean().optional().describe('Only show unmatched routes/requests (endpoints command).'),
338
- method: z.string().optional().describe('Filter by HTTP method (e.g. "GET", "POST") for endpoints.'),
339
- prefix: z.string().optional().describe('Filter routes/requests by path prefix (endpoints command).'),
340
- hide_uncertain: z.boolean().optional().describe('Hide uncertain (interpolated-path) bridges (endpoints command).')
341
-
342
- })).passthrough()
332
+ inputSchema: INPUT_SCHEMA,
333
+ annotations: {
334
+ title: 'Universal Code Navigator',
335
+ readOnlyHint: true,
336
+ destructiveHint: false,
337
+ idempotentHint: true,
338
+ openWorldHint: false,
339
+ },
343
340
  },
344
341
  async (args) => {
345
342
  const { command, project_dir, ...rawParams } = args;
@@ -521,8 +518,7 @@ server.registerTool(
521
518
  // ============================================================================
522
519
 
523
520
  async function main() {
524
- const transport = new StdioServerTransport();
525
- await server.connect(transport);
521
+ server.connect();
526
522
  // Print the running version so MCP-vs-CLI drift is visible (field-report #3:
527
523
  // a stale `npx -y ucn` cache can silently run an older engine than the CLI).
528
524
  console.error(`UCN MCP server v${require('../package.json').version} running on stdio`);