ast-mcp-server 0.5.0 → 0.6.0

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,8 +1,7 @@
1
- import path from "node:path";
2
- import { Node } from "ts-morph";
3
1
  import { z } from "zod";
4
- import { PaginationInputSchema, PaginationOutputSchema, paginate } from "../services/pagination.js";
2
+ import { PaginationInputSchema, PaginationOutputSchema } from "../services/pagination.js";
5
3
  import { findDeclarationByName, getSourceFileOrThrow, withProject } from "../services/project.js";
4
+ import { collectSymbolReferences } from "../services/references.js";
6
5
  import { errorResult, formattedResult, ToolOutputFormatInputSchema } from "./result.js";
7
6
  const ReferenceDetailSchema = z.enum(["locations", "context"]).default("locations");
8
7
  const AstFindReferencesInputSchema = z.object({
@@ -59,55 +58,19 @@ export function registerFindReferences(server) {
59
58
  const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
60
59
  const sourceFile = getSourceFileOrThrow(project, file_path);
61
60
  const node = findDeclarationByName(sourceFile, symbol_path);
62
- if (!Node.isReferenceFindable(node)) {
63
- throw new Error(`Symbol "${symbol_path}" (${node.getKindName()}) does not support semantic reference search.`);
64
- }
65
- const lineCache = new Map();
66
- const location = (referenceNode, isDeclaration) => {
67
- const referenceSource = referenceNode.getSourceFile();
68
- const absoluteFile = referenceSource.getFilePath();
69
- let lines = lineCache.get(absoluteFile);
70
- if (!lines) {
71
- lines = referenceSource.getFullText().split(/\r?\n/);
72
- lineCache.set(absoluteFile, lines);
73
- }
74
- const position = referenceSource.getLineAndColumnAtPos(referenceNode.getStart());
75
- return {
76
- file: path.relative(projectRoot, absoluteFile),
77
- line: position.line,
78
- column: position.column,
79
- kind: referenceNode.getParent()?.getKindName() ?? referenceNode.getKindName(),
80
- is_declaration: isDeclaration,
81
- context: (lines[position.line - 1] ?? "").trim().slice(0, 500),
82
- };
83
- };
84
- const references = node
85
- .findReferencesAsNodes()
86
- .map((reference) => location(reference, false));
87
- const declaration = location(node, true);
88
- const allLocations = include_declaration ? [declaration, ...references] : references;
89
- allLocations.sort((left, right) => `${left.file}:${left.line}:${left.column}:${left.is_declaration ? 0 : 1}`.localeCompare(`${right.file}:${right.line}:${right.column}:${right.is_declaration ? 0 : 1}`));
90
- const affectedFiles = [
91
- ...new Set([declaration, ...references].map((item) => item.file)),
92
- ].sort();
93
- const page = paginate(allLocations, offset, limit);
94
- const { items, ...metadata } = page;
61
+ const collected = collectSymbolReferences(node, projectRoot, symbol_path, include_declaration, detail, offset, limit);
62
+ const { references, ...metadata } = collected;
95
63
  return {
96
- symbol: symbol_path,
97
- include_declaration,
98
- declaration_count: 1,
99
- reference_count: references.length,
100
- affected_files: affectedFiles,
64
+ ...metadata,
101
65
  references: detail === "context"
102
- ? items
103
- : items.map(({ file, line, column, kind, is_declaration }) => ({
66
+ ? references
67
+ : references.map(({ file, line, column, kind, is_declaration }) => ({
104
68
  file,
105
69
  line,
106
70
  column,
107
71
  kind,
108
72
  is_declaration,
109
73
  })),
110
- ...metadata,
111
74
  };
112
75
  });
113
76
  const outputSchema = ReferenceOutputSchemas[detail];
@@ -0,0 +1,95 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_FILE_LINE_LIMIT, MAX_FILE_LINE_LIMIT, readFileSnapshot, } from "../services/file-snapshot.js";
3
+ import { buildFileOutline } from "../services/outline.js";
4
+ import { getSourceFileOrThrow, withProject } from "../services/project.js";
5
+ import { errorResult, structuredResult } from "./result.js";
6
+ const AstGetFileInputSchema = z.object({
7
+ project_root: z
8
+ .string()
9
+ .describe("Absolute project directory containing tsconfig.json, or the config path."),
10
+ file_path: z
11
+ .string()
12
+ .describe("Project-relative or unambiguous source file path included by the active tsconfig."),
13
+ offset: z.number().int().min(0).default(0).describe("Zero-based line offset for source mode."),
14
+ limit: z
15
+ .number()
16
+ .int()
17
+ .min(1)
18
+ .max(MAX_FILE_LINE_LIMIT)
19
+ .default(DEFAULT_FILE_LINE_LIMIT)
20
+ .describe(`Maximum source lines to return, capped at ${MAX_FILE_LINE_LIMIT}.`),
21
+ symbols_only: z
22
+ .boolean()
23
+ .default(false)
24
+ .describe("Return body-free symbol selectors and signatures instead of source lines."),
25
+ });
26
+ const FileRangeSchema = z.object({
27
+ offset: z.number().int().min(0),
28
+ limit: z.number().int().min(1).max(MAX_FILE_LINE_LIMIT),
29
+ total_lines: z.number().int().min(0),
30
+ });
31
+ const FileLineSchema = z.object({
32
+ line: z.number().int().positive(),
33
+ text: z.string(),
34
+ });
35
+ const OutlineSymbolSchema = z.object({
36
+ symbolPath: z.string(),
37
+ name: z.string(),
38
+ kind: z.string(),
39
+ signature: z.string(),
40
+ startLine: z.number().int().positive(),
41
+ endLine: z.number().int().positive(),
42
+ });
43
+ const FileSnapshotBaseSchema = {
44
+ file: z.string(),
45
+ file_hash: z.string().regex(/^[0-9a-f]{64}$/),
46
+ snapshot_state: z.enum(["fresh", "stale"]),
47
+ };
48
+ const AstGetFileOutputSchema = z.object({
49
+ mode: z.enum(["source", "symbols_only"]),
50
+ ...FileSnapshotBaseSchema,
51
+ range: FileRangeSchema.optional(),
52
+ lines: z.array(FileLineSchema).optional(),
53
+ symbols: z.array(OutlineSymbolSchema).optional(),
54
+ });
55
+ export function registerGetFile(server) {
56
+ server.registerTool("ast_get_file", {
57
+ title: "Get bounded source file",
58
+ description: "Returns exact bounded source lines for one TypeScript/JavaScript file, or body-free symbols when symbols_only is true. Read-only; freshness is reported separately from compiler diagnostics.",
59
+ inputSchema: AstGetFileInputSchema,
60
+ outputSchema: AstGetFileOutputSchema,
61
+ annotations: {
62
+ readOnlyHint: true,
63
+ destructiveHint: false,
64
+ idempotentHint: true,
65
+ openWorldHint: false,
66
+ },
67
+ }, async ({ project_root, file_path, offset, limit, symbols_only }) => {
68
+ try {
69
+ const structuredContent = await withProject(project_root, async ({ project, projectRoot }) => {
70
+ const snapshot = await readFileSnapshot(project, projectRoot, file_path, {
71
+ offset,
72
+ limit,
73
+ });
74
+ if (!symbols_only) {
75
+ return {
76
+ mode: "source",
77
+ ...snapshot,
78
+ };
79
+ }
80
+ const sourceFile = getSourceFileOrThrow(project, file_path);
81
+ return {
82
+ mode: "symbols_only",
83
+ file: snapshot.file,
84
+ file_hash: snapshot.file_hash,
85
+ snapshot_state: snapshot.snapshot_state,
86
+ symbols: buildFileOutline(sourceFile).symbols,
87
+ };
88
+ });
89
+ return structuredResult(structuredContent);
90
+ }
91
+ catch (error) {
92
+ return errorResult(error);
93
+ }
94
+ });
95
+ }
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ import { FRESHNESS_CAUSES, SNAPSHOT_STATES, TRUNCATION_REASONS, } from "../services/read-contracts.js";
3
+ import { getProjectStatus } from "../services/project.js";
4
+ import { errorResult, structuredResult } from "./result.js";
5
+ const AstGetProjectStatusInputSchema = z.object({
6
+ project_root: z
7
+ .string()
8
+ .describe("Absolute project directory containing tsconfig.json, or the config path."),
9
+ });
10
+ const TruncationMetadataSchema = z.object({
11
+ truncated: z.boolean(),
12
+ reason: z.enum(TRUNCATION_REASONS).nullable(),
13
+ });
14
+ const ProjectStatusOutputSchema = z.object({
15
+ project: z.object({
16
+ project_id: z.string().regex(/^project_[0-9a-f]{20}$/),
17
+ config_id: z
18
+ .string()
19
+ .regex(/^config_[0-9a-f]{20}$/)
20
+ .nullable(),
21
+ }),
22
+ state: z.enum(SNAPSHOT_STATES),
23
+ causes: z.array(z.enum(FRESHNESS_CAUSES)),
24
+ source_count: z.number().int().min(0),
25
+ indexed_count: z.number().int().min(0),
26
+ pending_files: z.array(z.string()),
27
+ pending_files_truncated: z.boolean(),
28
+ pending_files_truncation: TruncationMetadataSchema,
29
+ pending_files_filename_truncation: TruncationMetadataSchema,
30
+ compiler: z.object({ state: z.enum(["ready", "rebuilding", "failed"]) }),
31
+ index: z.object({ state: z.literal("disabled") }),
32
+ operation_queue: z.object({
33
+ state: z.enum(["idle", "queued", "running"]),
34
+ active_operations: z.number().int().min(0),
35
+ queued_operations: z.number().int().min(0),
36
+ }),
37
+ watcher: z.object({ state: z.enum(["disabled", "ready", "failed"]) }),
38
+ last_successful_sync_at: z.string().nullable(),
39
+ last_successful_index_at: z.null(),
40
+ source_snapshot_fingerprint: z.string().nullable(),
41
+ config_snapshot_fingerprint: z.string().nullable(),
42
+ canonical_snapshot_fingerprint: z.string().nullable(),
43
+ degraded_errors: z.array(z.string()),
44
+ degraded_errors_truncation: TruncationMetadataSchema,
45
+ degraded_errors_text_truncation: TruncationMetadataSchema,
46
+ });
47
+ export function registerGetProjectStatus(server) {
48
+ server.registerTool("ast_get_project_status", {
49
+ title: "Get project freshness status",
50
+ description: "Returns bounded, read-only compiler freshness and project status without exposing filesystem identities or credentials.",
51
+ inputSchema: AstGetProjectStatusInputSchema,
52
+ outputSchema: ProjectStatusOutputSchema,
53
+ annotations: {
54
+ readOnlyHint: true,
55
+ destructiveHint: false,
56
+ idempotentHint: true,
57
+ openWorldHint: false,
58
+ },
59
+ }, async ({ project_root }) => {
60
+ try {
61
+ const output = await getProjectStatus(project_root);
62
+ return structuredResult({ ...output });
63
+ }
64
+ catch (error) {
65
+ return errorResult(error);
66
+ }
67
+ });
68
+ }
@@ -1,9 +1,7 @@
1
- import path from "node:path";
2
1
  import { z } from "zod";
3
- import { buildFileOutline } from "../services/outline.js";
4
2
  import { createPaginationInputSchema, PaginationOutputSchema, paginate, } from "../services/pagination.js";
5
3
  import { withProject } from "../services/project.js";
6
- import { collectSymbols, symbolMatchRank } from "../services/symbols.js";
4
+ import { searchProjectSymbols } from "../services/symbols.js";
7
5
  import { errorResult, formattedResult, ToolOutputFormatInputSchema } from "./result.js";
8
6
  const SEARCH_DEFAULT_LIMIT = 20;
9
7
  const SearchDetailSchema = z.enum(["selectors", "summary", "full"]).default("summary");
@@ -92,57 +90,10 @@ export function registerSearchSymbols(server) {
92
90
  try {
93
91
  const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
94
92
  const startedAt = performance.now();
95
- const normalizedQuery = query.toLowerCase();
96
- const normalizedFileFilter = file_filter?.toLowerCase();
97
- const kindSet = kinds ? new Set(kinds) : undefined;
98
- const matches = [];
99
- for (const sourceFile of project.getSourceFiles()) {
100
- const file = path.relative(projectRoot, sourceFile.getFilePath());
101
- if (normalizedFileFilter && !file.toLowerCase().includes(normalizedFileFilter)) {
102
- continue;
103
- }
104
- const matchingSymbols = collectSymbols(sourceFile).filter((symbol) => {
105
- const selector = `${symbol.symbolPath}@${symbol.line}`.toLowerCase();
106
- return ((!kindSet || kindSet.has(symbol.kind)) &&
107
- (symbol.name.toLowerCase().includes(normalizedQuery) ||
108
- symbol.symbolPath.toLowerCase().includes(normalizedQuery) ||
109
- selector.includes(normalizedQuery)));
110
- });
111
- if (matchingSymbols.length === 0)
112
- continue;
113
- const signatures = new Map(buildFileOutline(sourceFile).symbols.map((symbol) => [
114
- `${symbol.symbolPath}@${symbol.startLine}`,
115
- symbol.signature,
116
- ]));
117
- for (const symbol of matchingSymbols) {
118
- matches.push({
119
- file,
120
- symbol_path: symbol.symbolPath,
121
- selector: `${symbol.symbolPath}@${symbol.line}`,
122
- name: symbol.name,
123
- kind: symbol.kind,
124
- line: symbol.line,
125
- signature: signatures.get(`${symbol.symbolPath}@${symbol.line}`) ?? symbol.node.getText(),
126
- });
127
- }
128
- }
129
- matches.sort((left, right) => {
130
- const rank = symbolMatchRank(query, {
131
- symbolPath: left.symbol_path,
132
- name: left.name,
133
- line: left.line,
134
- }) -
135
- symbolMatchRank(query, {
136
- symbolPath: right.symbol_path,
137
- name: right.name,
138
- line: right.line,
139
- });
140
- if (rank !== 0)
141
- return rank;
142
- return (left.file.localeCompare(right.file) ||
143
- left.line - right.line ||
144
- left.symbol_path.localeCompare(right.symbol_path) ||
145
- left.kind.localeCompare(right.kind));
93
+ const matches = searchProjectSymbols(project, projectRoot, {
94
+ query,
95
+ kinds,
96
+ fileFilter: file_filter,
146
97
  });
147
98
  const page = paginate(matches, offset, limit);
148
99
  const { items, ...metadata } = page;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ast-mcp-server",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Structural code MCP server and batch CLI for compact TypeScript/JavaScript reads and hash-bound AST edits.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -56,6 +56,7 @@
56
56
  "benchmark:batch": "yarn build && node scripts/benchmark-batch.mjs",
57
57
  "benchmark:formats": "yarn build && node scripts/benchmark-formats.mjs",
58
58
  "benchmark:shapes": "yarn build && node scripts/benchmark-result-shapes.mjs",
59
+ "benchmark:agent-workflows": "yarn build && node scripts/benchmark-agent-workflows.mjs",
59
60
  "lint": "eslint src test scripts vitest.config.ts eslint.config.js",
60
61
  "format": "prettier --write .",
61
62
  "format:check": "prettier --check .",
@@ -17,6 +17,7 @@ metadata:
17
17
  Usar las tools del servidor MCP `ast` en proyectos TypeScript/JavaScript con `tsconfig.json` cuando haya que:
18
18
 
19
19
  - orientarse en archivos o módulos grandes/desconocidos;
20
+ - leer rangos acotados del source exacto de un archivo conocido;
20
21
  - encontrar declaraciones o referencias reales;
21
22
  - medir el radio de impacto de un rename;
22
23
  - renombrar un símbolo en varios archivos;
@@ -40,16 +41,22 @@ El setup falla cerrado ante un registro `ast` conflictivo. No eliminar ni reempl
40
41
  ## Flujo compacto de lectura
41
42
 
42
43
  1. `ast_list_files` para descubrir archivos. Paginar; no pedir todo un monorepo si alcanza con un filtro.
43
- 2. `ast_search_symbols` si todavía no se conoce el archivo o el selector exacto.
44
- 3. `ast_get_outline` para ver contratos sin cuerpos. No pedir `include_symbols` salvo que haga falta la metadata detallada.
45
- 4. `ast_get_symbol_source` solo para las declaraciones cuya implementación haya que inspeccionar.
46
- 5. `ast_find_references` antes de renames o cambios con impacto cross-file.
47
- 6. `ast_get_diagnostics` para establecer y verificar el estado del proyecto.
44
+ 2. `ast_explore` para una pregunta de lectura que combine descubrimiento y evidencia. Usar `summary` por defecto, `context` para source seleccionado y `full` para source más referencias.
45
+ 3. `ast_get_file` si ya se conoce el archivo y hace falta source exacto. Usar `offset`/`limit`; no cargar el archivo completo por defecto.
46
+ 4. `ast_search_symbols` si todavía no se conoce el archivo o el selector exacto.
47
+ 5. `ast_get_outline` para ver contratos sin cuerpos. No pedir `include_symbols` salvo que haga falta la metadata detallada.
48
+ 6. `ast_get_symbol_source` solo para las declaraciones cuya implementación haya que inspeccionar.
49
+ 7. `ast_find_references` antes de renames o cambios con impacto cross-file.
50
+ 8. `ast_get_diagnostics` para establecer y verificar el estado del proyecto.
48
51
 
49
52
  `ast_search_symbols` devuelve por default hasta 20 records `summary` rankeados. Su campo `selector` es el valor que se pasa como `symbol_path` a la siguiente tool; pedir `detail: "selectors"` para routing puro o `detail: "full", limit: 100` para el perfil v0.4.0. `ast_find_references` devuelve `detail: "locations"` por default; expandir a `detail: "context"` únicamente cuando la línea fuente aporte evidencia necesaria.
50
53
 
51
54
  `file_path` debe ser preferentemente relativo al proyecto. Si un suffix coincide con varios archivos, la tool falla y devuelve candidatos en vez de elegir uno silenciosamente.
52
55
 
56
+ `ast_get_file` es read-only y solo acepta archivos incluidos por el tsconfig activo. Su modo normal devuelve líneas exactas, paginadas y numeradas desde 1, junto con el hash SHA-256 de los bytes actuales. `symbols_only: true` devuelve selectors y signatures sin cuerpos. `snapshot_state: "fresh"` describe sincronización con el snapshot del compilador, no ausencia de diagnostics; consultar `ast_get_diagnostics` por separado.
57
+
58
+ `ast_explore` es read-only y no prepara ni aplica operaciones. Sus selectors son directamente reutilizables por las tools primitivas y de mutación. La respuesta declara `freshness`, `completeness`, `truncation`, `unresolved` y `budget`; si el límite de bytes impide incluir todo el contexto, el resultado queda marcado como incompleto.
59
+
53
60
  Los clientes pueden prefijar los nombres publicados (`ast_*`) según su convención MCP; elegir por el nombre base y el schema, no adivinar el prefijo.
54
61
 
55
62
  ## Formato de resultado para el modelo