ast-mcp-server 0.3.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.
- package/CHANGELOG.md +27 -0
- package/LICENSE +7 -0
- package/README.md +382 -0
- package/benchmark/README.md +86 -0
- package/dist/batch/runner.js +228 -0
- package/dist/batch/schema.js +159 -0
- package/dist/cli.js +300 -0
- package/dist/index.js +13 -0
- package/dist/server.js +28 -0
- package/dist/services/agent-setup.js +346 -0
- package/dist/services/diagnostics.js +51 -0
- package/dist/services/operation-plan-file.js +191 -0
- package/dist/services/operations.js +622 -0
- package/dist/services/outline.js +233 -0
- package/dist/services/pagination.js +33 -0
- package/dist/services/project.js +123 -0
- package/dist/services/runtime-state.js +75 -0
- package/dist/services/setup-wizard.js +113 -0
- package/dist/services/skill-installer.js +127 -0
- package/dist/services/symbols.js +117 -0
- package/dist/services/workspace.js +86 -0
- package/dist/tools/apply_operation.js +36 -0
- package/dist/tools/find_references.js +97 -0
- package/dist/tools/get_diagnostics.js +70 -0
- package/dist/tools/get_operation_preview.js +33 -0
- package/dist/tools/get_outline.js +58 -0
- package/dist/tools/get_symbol_source.js +43 -0
- package/dist/tools/list_files.js +51 -0
- package/dist/tools/operation-schema.js +64 -0
- package/dist/tools/rename_symbol.js +49 -0
- package/dist/tools/replace_symbol_body.js +49 -0
- package/dist/tools/result.js +14 -0
- package/dist/tools/search_symbols.js +101 -0
- package/package.json +78 -0
- package/skills/structural-code-editing/SKILL.md +119 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { PaginationInputSchema, PaginationOutputSchema, paginate } from "../services/pagination.js";
|
|
4
|
+
import { withProject } from "../services/project.js";
|
|
5
|
+
import { errorResult, structuredResult } from "./result.js";
|
|
6
|
+
const AstListFilesInputSchema = z.object({
|
|
7
|
+
project_root: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe("Absolute project directory containing tsconfig.json, or the config path."),
|
|
10
|
+
glob_filter: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Optional case-insensitive substring used to filter project-relative paths."),
|
|
14
|
+
...PaginationInputSchema,
|
|
15
|
+
});
|
|
16
|
+
const AstListFilesOutputSchema = z.object({
|
|
17
|
+
files: z.array(z.string()),
|
|
18
|
+
...PaginationOutputSchema,
|
|
19
|
+
});
|
|
20
|
+
export function registerListFiles(server) {
|
|
21
|
+
server.registerTool("ast_list_files", {
|
|
22
|
+
title: "List TypeScript/JavaScript project files",
|
|
23
|
+
description: "Lists source files included by the project's tsconfig in deterministic, project-relative, paginated form.",
|
|
24
|
+
inputSchema: AstListFilesInputSchema,
|
|
25
|
+
outputSchema: AstListFilesOutputSchema,
|
|
26
|
+
annotations: {
|
|
27
|
+
readOnlyHint: true,
|
|
28
|
+
destructiveHint: false,
|
|
29
|
+
idempotentHint: true,
|
|
30
|
+
openWorldHint: false,
|
|
31
|
+
},
|
|
32
|
+
}, async ({ project_root, glob_filter, offset, limit }) => {
|
|
33
|
+
try {
|
|
34
|
+
const output = await withProject(project_root, ({ project, projectRoot }) => {
|
|
35
|
+
const filter = glob_filter?.toLowerCase();
|
|
36
|
+
const files = project
|
|
37
|
+
.getSourceFiles()
|
|
38
|
+
.map((sourceFile) => path.relative(projectRoot, sourceFile.getFilePath()))
|
|
39
|
+
.filter((file) => !filter || file.toLowerCase().includes(filter))
|
|
40
|
+
.sort((left, right) => left.localeCompare(right));
|
|
41
|
+
const page = paginate(files, offset, limit);
|
|
42
|
+
const { items, ...metadata } = page;
|
|
43
|
+
return { files: items, ...metadata };
|
|
44
|
+
});
|
|
45
|
+
return structuredResult(output);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return errorResult(error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const NormalizedDiagnosticSchema = z.object({
|
|
3
|
+
code: z.number().int(),
|
|
4
|
+
category: z.string(),
|
|
5
|
+
file: z.string().nullable(),
|
|
6
|
+
line: z.number().int().positive().nullable(),
|
|
7
|
+
column: z.number().int().positive().nullable(),
|
|
8
|
+
message: z.string(),
|
|
9
|
+
});
|
|
10
|
+
export const PreparedOperationOutputSchema = z.object({
|
|
11
|
+
operation_id: z.string().uuid(),
|
|
12
|
+
plan_hash: z.string().length(64),
|
|
13
|
+
kind: z.enum(["rename_symbol", "replace_symbol_body"]),
|
|
14
|
+
status: z.literal("prepared"),
|
|
15
|
+
project_root: z.string(),
|
|
16
|
+
created_at: z.string(),
|
|
17
|
+
expires_at: z.string(),
|
|
18
|
+
affected_files: z.array(z.object({
|
|
19
|
+
file: z.string(),
|
|
20
|
+
original_hash: z.string(),
|
|
21
|
+
updated_hash: z.string(),
|
|
22
|
+
})),
|
|
23
|
+
reference_count: z.number().int().min(0),
|
|
24
|
+
workspace_hash: z.string().length(64),
|
|
25
|
+
workspace_file_count: z.number().int().positive(),
|
|
26
|
+
diagnostics: z.object({
|
|
27
|
+
added: z.array(NormalizedDiagnosticSchema),
|
|
28
|
+
removed: z.array(NormalizedDiagnosticSchema),
|
|
29
|
+
added_errors: z.array(NormalizedDiagnosticSchema),
|
|
30
|
+
}),
|
|
31
|
+
allow_new_errors: z.boolean(),
|
|
32
|
+
blocked: z.boolean(),
|
|
33
|
+
block_reason: z.string().nullable(),
|
|
34
|
+
preview: z.string().nullable(),
|
|
35
|
+
preview_truncated: z.boolean(),
|
|
36
|
+
});
|
|
37
|
+
export function serializePreparedOperation(operation) {
|
|
38
|
+
if (operation.status !== "prepared") {
|
|
39
|
+
throw new Error(`Operation ${operation.operation_id} is not in prepared state.`);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
operation_id: operation.operation_id,
|
|
43
|
+
plan_hash: operation.plan_hash,
|
|
44
|
+
kind: operation.kind,
|
|
45
|
+
status: "prepared",
|
|
46
|
+
project_root: operation.project_root,
|
|
47
|
+
created_at: operation.created_at,
|
|
48
|
+
expires_at: operation.expires_at,
|
|
49
|
+
affected_files: operation.affected_files,
|
|
50
|
+
reference_count: operation.reference_count,
|
|
51
|
+
workspace_hash: operation.workspace_hash,
|
|
52
|
+
workspace_file_count: operation.workspace_file_count,
|
|
53
|
+
diagnostics: {
|
|
54
|
+
added: operation.diagnostics.added,
|
|
55
|
+
removed: operation.diagnostics.removed,
|
|
56
|
+
added_errors: operation.diagnostics.addedErrors,
|
|
57
|
+
},
|
|
58
|
+
allow_new_errors: operation.allow_new_errors,
|
|
59
|
+
blocked: operation.blocked,
|
|
60
|
+
block_reason: operation.block_reason,
|
|
61
|
+
preview: operation.preview,
|
|
62
|
+
preview_truncated: operation.preview_truncated,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { prepareRename } from "../services/operations.js";
|
|
3
|
+
import { PreparedOperationOutputSchema, serializePreparedOperation } from "./operation-schema.js";
|
|
4
|
+
import { errorResult, structuredResult } from "./result.js";
|
|
5
|
+
const AstRenameSymbolInputSchema = z.object({
|
|
6
|
+
project_root: z.string(),
|
|
7
|
+
file_path: z.string(),
|
|
8
|
+
symbol_path: z.string(),
|
|
9
|
+
new_name: z.string().min(1).describe("New identifier only, without a container prefix."),
|
|
10
|
+
dry_run: z
|
|
11
|
+
.boolean()
|
|
12
|
+
.default(true)
|
|
13
|
+
.describe("Compatibility field. Direct application is disabled; this tool always prepares."),
|
|
14
|
+
allow_new_errors: z
|
|
15
|
+
.boolean()
|
|
16
|
+
.default(false)
|
|
17
|
+
.describe("Permit a plan with new TypeScript errors after explicit review."),
|
|
18
|
+
});
|
|
19
|
+
export function registerRenameSymbol(server) {
|
|
20
|
+
server.registerTool("ast_rename_symbol", {
|
|
21
|
+
title: "Prepare a project-wide structural rename",
|
|
22
|
+
description: "Prepares an exact hash-bound rename plan without writing. Review the preview and apply its operation_id with ast_apply_operation.",
|
|
23
|
+
inputSchema: AstRenameSymbolInputSchema,
|
|
24
|
+
outputSchema: PreparedOperationOutputSchema,
|
|
25
|
+
annotations: {
|
|
26
|
+
readOnlyHint: true,
|
|
27
|
+
destructiveHint: false,
|
|
28
|
+
idempotentHint: false,
|
|
29
|
+
openWorldHint: false,
|
|
30
|
+
},
|
|
31
|
+
}, async ({ project_root, file_path, symbol_path, new_name, dry_run, allow_new_errors }) => {
|
|
32
|
+
try {
|
|
33
|
+
if (!dry_run) {
|
|
34
|
+
throw new Error("Direct rename application is disabled. Prepare with dry_run=true, then call ast_apply_operation with the returned operation_id.");
|
|
35
|
+
}
|
|
36
|
+
const operation = await prepareRename({
|
|
37
|
+
projectRoot: project_root,
|
|
38
|
+
filePath: file_path,
|
|
39
|
+
symbolPath: symbol_path,
|
|
40
|
+
newName: new_name,
|
|
41
|
+
allowNewErrors: allow_new_errors,
|
|
42
|
+
});
|
|
43
|
+
return structuredResult(serializePreparedOperation(operation));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return errorResult(error);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { prepareReplaceBody } from "../services/operations.js";
|
|
3
|
+
import { PreparedOperationOutputSchema, serializePreparedOperation } from "./operation-schema.js";
|
|
4
|
+
import { errorResult, structuredResult } from "./result.js";
|
|
5
|
+
const AstReplaceSymbolBodyInputSchema = z.object({
|
|
6
|
+
project_root: z.string(),
|
|
7
|
+
file_path: z.string(),
|
|
8
|
+
symbol_path: z.string(),
|
|
9
|
+
new_body: z.string().describe("New body text without the enclosing braces."),
|
|
10
|
+
dry_run: z
|
|
11
|
+
.boolean()
|
|
12
|
+
.default(true)
|
|
13
|
+
.describe("Compatibility field. Direct application is disabled; this tool always prepares."),
|
|
14
|
+
allow_new_errors: z
|
|
15
|
+
.boolean()
|
|
16
|
+
.default(false)
|
|
17
|
+
.describe("Permit a plan with new TypeScript errors after explicit review."),
|
|
18
|
+
});
|
|
19
|
+
export function registerReplaceSymbolBody(server) {
|
|
20
|
+
server.registerTool("ast_replace_symbol_body", {
|
|
21
|
+
title: "Prepare an exact function-body replacement",
|
|
22
|
+
description: "Prepares a hash-bound body replacement while preserving the signature. Supports declarations, methods, accessors and function-valued variables/properties.",
|
|
23
|
+
inputSchema: AstReplaceSymbolBodyInputSchema,
|
|
24
|
+
outputSchema: PreparedOperationOutputSchema,
|
|
25
|
+
annotations: {
|
|
26
|
+
readOnlyHint: true,
|
|
27
|
+
destructiveHint: false,
|
|
28
|
+
idempotentHint: false,
|
|
29
|
+
openWorldHint: false,
|
|
30
|
+
},
|
|
31
|
+
}, async ({ project_root, file_path, symbol_path, new_body, dry_run, allow_new_errors }) => {
|
|
32
|
+
try {
|
|
33
|
+
if (!dry_run) {
|
|
34
|
+
throw new Error("Direct body replacement is disabled. Prepare with dry_run=true, then call ast_apply_operation with the returned operation_id.");
|
|
35
|
+
}
|
|
36
|
+
const operation = await prepareReplaceBody({
|
|
37
|
+
projectRoot: project_root,
|
|
38
|
+
filePath: file_path,
|
|
39
|
+
symbolPath: symbol_path,
|
|
40
|
+
newBody: new_body,
|
|
41
|
+
allowNewErrors: allow_new_errors,
|
|
42
|
+
});
|
|
43
|
+
return structuredResult(serializePreparedOperation(operation));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return errorResult(error);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function structuredResult(structuredContent) {
|
|
2
|
+
return { content: [], structuredContent };
|
|
3
|
+
}
|
|
4
|
+
export function errorResult(error) {
|
|
5
|
+
return {
|
|
6
|
+
content: [
|
|
7
|
+
{
|
|
8
|
+
type: "text",
|
|
9
|
+
text: error instanceof Error ? error.message : String(error),
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
isError: true,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { buildFileOutline } from "../services/outline.js";
|
|
4
|
+
import { PaginationInputSchema, PaginationOutputSchema, paginate } from "../services/pagination.js";
|
|
5
|
+
import { withProject } from "../services/project.js";
|
|
6
|
+
import { collectSymbols } from "../services/symbols.js";
|
|
7
|
+
import { errorResult, structuredResult } from "./result.js";
|
|
8
|
+
const AstSearchSymbolsInputSchema = z.object({
|
|
9
|
+
project_root: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Absolute project directory containing tsconfig.json, or the config path."),
|
|
12
|
+
query: z
|
|
13
|
+
.string()
|
|
14
|
+
.min(1)
|
|
15
|
+
.describe("Case-insensitive substring matched against name and symbol path."),
|
|
16
|
+
kinds: z
|
|
17
|
+
.array(z.string())
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Optional exact ts-morph syntax kinds, such as ClassDeclaration or MethodDeclaration."),
|
|
20
|
+
file_filter: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Optional case-insensitive substring matched against project-relative file paths."),
|
|
24
|
+
...PaginationInputSchema,
|
|
25
|
+
});
|
|
26
|
+
const SearchSymbolSchema = z.object({
|
|
27
|
+
file: z.string(),
|
|
28
|
+
symbol_path: z.string(),
|
|
29
|
+
selector: z.string(),
|
|
30
|
+
name: z.string(),
|
|
31
|
+
kind: z.string(),
|
|
32
|
+
line: z.number().int().positive(),
|
|
33
|
+
signature: z.string(),
|
|
34
|
+
});
|
|
35
|
+
const AstSearchSymbolsOutputSchema = z.object({
|
|
36
|
+
symbols: z.array(SearchSymbolSchema),
|
|
37
|
+
duration_ms: z.number().min(0),
|
|
38
|
+
...PaginationOutputSchema,
|
|
39
|
+
});
|
|
40
|
+
export function registerSearchSymbols(server) {
|
|
41
|
+
server.registerTool("ast_search_symbols", {
|
|
42
|
+
title: "Search project symbols",
|
|
43
|
+
description: "Searches declarations structurally and returns exact file/symbol selectors that can be passed to the other AST tools.",
|
|
44
|
+
inputSchema: AstSearchSymbolsInputSchema,
|
|
45
|
+
outputSchema: AstSearchSymbolsOutputSchema,
|
|
46
|
+
annotations: {
|
|
47
|
+
readOnlyHint: true,
|
|
48
|
+
destructiveHint: false,
|
|
49
|
+
idempotentHint: true,
|
|
50
|
+
openWorldHint: false,
|
|
51
|
+
},
|
|
52
|
+
}, async ({ project_root, query, kinds, file_filter, offset, limit }) => {
|
|
53
|
+
try {
|
|
54
|
+
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
55
|
+
const startedAt = performance.now();
|
|
56
|
+
const normalizedQuery = query.toLowerCase();
|
|
57
|
+
const normalizedFileFilter = file_filter?.toLowerCase();
|
|
58
|
+
const kindSet = kinds ? new Set(kinds) : undefined;
|
|
59
|
+
const matches = [];
|
|
60
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
61
|
+
const file = path.relative(projectRoot, sourceFile.getFilePath());
|
|
62
|
+
if (normalizedFileFilter && !file.toLowerCase().includes(normalizedFileFilter)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const matchingSymbols = collectSymbols(sourceFile).filter((symbol) => (!kindSet || kindSet.has(symbol.kind)) &&
|
|
66
|
+
(symbol.name.toLowerCase().includes(normalizedQuery) ||
|
|
67
|
+
symbol.symbolPath.toLowerCase().includes(normalizedQuery)));
|
|
68
|
+
if (matchingSymbols.length === 0)
|
|
69
|
+
continue;
|
|
70
|
+
const signatures = new Map(buildFileOutline(sourceFile).symbols.map((symbol) => [
|
|
71
|
+
`${symbol.symbolPath}@${symbol.startLine}`,
|
|
72
|
+
symbol.signature,
|
|
73
|
+
]));
|
|
74
|
+
for (const symbol of matchingSymbols) {
|
|
75
|
+
matches.push({
|
|
76
|
+
file,
|
|
77
|
+
symbol_path: symbol.symbolPath,
|
|
78
|
+
selector: `${symbol.symbolPath}@${symbol.line}`,
|
|
79
|
+
name: symbol.name,
|
|
80
|
+
kind: symbol.kind,
|
|
81
|
+
line: symbol.line,
|
|
82
|
+
signature: signatures.get(`${symbol.symbolPath}@${symbol.line}`) ?? symbol.node.getText(),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
matches.sort((left, right) => `${left.file}:${left.line}:${left.symbol_path}`.localeCompare(`${right.file}:${right.line}:${right.symbol_path}`));
|
|
87
|
+
const page = paginate(matches, offset, limit);
|
|
88
|
+
const { items, ...metadata } = page;
|
|
89
|
+
return {
|
|
90
|
+
symbols: items,
|
|
91
|
+
duration_ms: performance.now() - startedAt,
|
|
92
|
+
...metadata,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
return structuredResult(structuredContent);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return errorResult(error);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ast-mcp-server",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Structural code MCP server and batch CLI for compact TypeScript/JavaScript reads and hash-bound AST edits.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"typescript",
|
|
8
|
+
"javascript",
|
|
9
|
+
"ast",
|
|
10
|
+
"ts-morph",
|
|
11
|
+
"coding-agent",
|
|
12
|
+
"refactoring"
|
|
13
|
+
],
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"author": "Yail Peralta",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/yailPeralta/ast-mcp-server.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/yailPeralta/ast-mcp-server#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/yailPeralta/ast-mcp-server/issues"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"packageManager": "yarn@4.15.0",
|
|
29
|
+
"main": "dist/index.js",
|
|
30
|
+
"bin": {
|
|
31
|
+
"ast-mcp-server": "dist/index.js",
|
|
32
|
+
"ast-tool": "dist/cli.js"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"skills",
|
|
37
|
+
"README.md",
|
|
38
|
+
"CHANGELOG.md"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20.19"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"test:mcp": "yarn build && node scripts/mcp-smoke.mjs",
|
|
49
|
+
"test:cli": "yarn build && node scripts/cli-smoke.mjs",
|
|
50
|
+
"test:package": "node scripts/package-smoke.mjs",
|
|
51
|
+
"benchmark": "yarn build && node scripts/benchmark.mjs",
|
|
52
|
+
"benchmark:corpus": "yarn build && node scripts/benchmark-corpus.mjs",
|
|
53
|
+
"benchmark:batch": "yarn build && node scripts/benchmark-batch.mjs",
|
|
54
|
+
"lint": "eslint src test scripts vitest.config.ts eslint.config.js",
|
|
55
|
+
"format": "prettier --write .",
|
|
56
|
+
"format:check": "prettier --check .",
|
|
57
|
+
"prepack": "yarn build",
|
|
58
|
+
"audit": "yarn npm audit --all --recursive",
|
|
59
|
+
"setup": "yarn build && node dist/cli.js setup",
|
|
60
|
+
"start": "node dist/index.js",
|
|
61
|
+
"dev": "tsc --watch"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
65
|
+
"diff": "^9.0.0",
|
|
66
|
+
"ts-morph": "24.0.0",
|
|
67
|
+
"zod": "^3.23.8"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@eslint/js": "^10.0.1",
|
|
71
|
+
"@types/node": "^22.5.0",
|
|
72
|
+
"eslint": "^10.8.0",
|
|
73
|
+
"prettier": "^3.9.6",
|
|
74
|
+
"typescript": "^5.6.0",
|
|
75
|
+
"typescript-eslint": "^8.65.0",
|
|
76
|
+
"vitest": "^4.1.10"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: structural-code-editing
|
|
3
|
+
description: Leer, navegar y editar proyectos TypeScript/JavaScript mediante el resolver real del compilador y operaciones AST preparadas, revisadas y vinculadas por hash.
|
|
4
|
+
version: "3.3.0"
|
|
5
|
+
author: "yail"
|
|
6
|
+
license: "ISC"
|
|
7
|
+
metadata:
|
|
8
|
+
hermes:
|
|
9
|
+
tags: ["typescript", "javascript", "ast", "refactoring", "mcp"]
|
|
10
|
+
homepage: "https://github.com/yailPeralta/ast-mcp-server"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Edición estructural con el servidor MCP `ast`
|
|
14
|
+
|
|
15
|
+
## Cuándo usarlo
|
|
16
|
+
|
|
17
|
+
Usar las tools del servidor MCP `ast` en proyectos TypeScript/JavaScript con `tsconfig.json` cuando haya que:
|
|
18
|
+
|
|
19
|
+
- orientarse en archivos o módulos grandes/desconocidos;
|
|
20
|
+
- encontrar declaraciones o referencias reales;
|
|
21
|
+
- medir el radio de impacto de un rename;
|
|
22
|
+
- renombrar un símbolo en varios archivos;
|
|
23
|
+
- reemplazar solo el cuerpo de una función, método, accessor o callable property;
|
|
24
|
+
- obtener diagnostics sin cargar el proyecto como texto en el contexto.
|
|
25
|
+
|
|
26
|
+
Para configs, Markdown, comentarios o una edición textual trivial en un archivo ya conocido, usar las tools normales de archivos. El AST no cobra alquiler, pero cada roundtrip sí.
|
|
27
|
+
|
|
28
|
+
## Preflight de disponibilidad
|
|
29
|
+
|
|
30
|
+
La presencia de este skill no demuestra que el transporte MCP esté configurado. Si las tools `ast_*` no aparecen, no simularlas con búsquedas textuales mientras se afirma que son estructurales:
|
|
31
|
+
|
|
32
|
+
- desde el checkout del servidor, ejecutar `yarn setup` para detectar agentes, registrar el MCP e instalar el skill;
|
|
33
|
+
- desde un paquete instalado, ejecutar `ast-tool setup`;
|
|
34
|
+
- verificar Claude Code con `/mcp` o `claude mcp get ast`;
|
|
35
|
+
- verificar Hermes con `hermes mcp test ast` y recargar la sesión si acaba de cambiar la configuración.
|
|
36
|
+
|
|
37
|
+
El setup falla cerrado ante un registro `ast` conflictivo. No eliminar ni reemplazar una configuración existente sin revisión explícita.
|
|
38
|
+
|
|
39
|
+
## Flujo compacto de lectura
|
|
40
|
+
|
|
41
|
+
1. `ast_list_files` para descubrir archivos. Paginar; no pedir todo un monorepo si alcanza con un filtro.
|
|
42
|
+
2. `ast_search_symbols` si todavía no se conoce el archivo o el selector exacto.
|
|
43
|
+
3. `ast_get_outline` para ver contratos sin cuerpos. No pedir `include_symbols` salvo que haga falta la metadata detallada.
|
|
44
|
+
4. `ast_get_symbol_source` solo para las declaraciones cuya implementación haya que inspeccionar.
|
|
45
|
+
5. `ast_find_references` antes de renames o cambios con impacto cross-file.
|
|
46
|
+
6. `ast_get_diagnostics` para establecer y verificar el estado del proyecto.
|
|
47
|
+
|
|
48
|
+
`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.
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
|
|
52
|
+
## Batch CLI para clientes con Bash
|
|
53
|
+
|
|
54
|
+
Cuando un pipeline conocido requiere varias llamadas MCP dependientes y el cliente tiene Bash, usar `ast-tool run pipeline.json` para colapsar los roundtrips del modelo. No usar batch para exploración abierta ni asumir que reduce el trabajo interno del compilador.
|
|
55
|
+
|
|
56
|
+
- Encadenar outputs previos con objetos `{ "$ref": "#/steps/id/campo" }`.
|
|
57
|
+
- Usar `foreach` más `{ "$item": "/campo" }` solamente para lecturas acotadas.
|
|
58
|
+
- Definir `emit` para que los resultados intermedios no entren al contexto.
|
|
59
|
+
- Mantener paginación y filtros: batch elimina roundtrips, no vuelve razonable leer un monorepo entero.
|
|
60
|
+
- No generar JavaScript/eval dentro del documento; el contrato es declarativo y limitado.
|
|
61
|
+
|
|
62
|
+
`ast-tool validate pipeline.json` valida schema, orden de referencias y política sin cargar el proyecto. Límites por defecto: 50 steps, 500 invocaciones, 200 items por foreach, concurrencia 4 (máximo 16), input 1 MiB, 10 MiB por resultado retenido/output y 50 MiB de contexto intermedio acumulado.
|
|
63
|
+
|
|
64
|
+
## Flujo obligatorio de mutación
|
|
65
|
+
|
|
66
|
+
Las tools de rename y reemplazo **solo preparan**. `dry_run: false` directo está deshabilitado.
|
|
67
|
+
|
|
68
|
+
1. Llamar `ast_rename_symbol` o `ast_replace_symbol_body` con `dry_run: true`.
|
|
69
|
+
2. Revisar:
|
|
70
|
+
- `affected_files`;
|
|
71
|
+
- summaries y previews;
|
|
72
|
+
- diagnostics agregados/removidos;
|
|
73
|
+
- `blocked` y el valor visible de `allow_new_errors`.
|
|
74
|
+
3. Si el preview inline está truncado o hay varios archivos, pedir diffs completos con `ast_get_operation_preview`.
|
|
75
|
+
4. Aplicar con `ast_apply_operation`, pasando **ambos**:
|
|
76
|
+
- `operation_id`;
|
|
77
|
+
- `plan_hash`.
|
|
78
|
+
5. Ejecutar el typecheck/build/test canónico del proyecto después del apply.
|
|
79
|
+
|
|
80
|
+
Nunca reconstruir el contenido a aplicar a partir del diff. Apply escribe los postimages exactos retenidos en el plan.
|
|
81
|
+
|
|
82
|
+
### Mutaciones desde el CLI
|
|
83
|
+
|
|
84
|
+
Un batch puede terminar en una sola preparación, pero nunca puede incluir `ast_apply_operation`. El resultado expone `operation_id`, `plan_hash` y `plan_file` en el nivel superior aunque `emit` los omita; revisar preview, diagnostics y affected files, y aplicar en otra llamada:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
ast-tool apply <plan_file> --plan-hash <reviewed_sha256>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Los planes CLI sobreviven al proceso bajo `${XDG_STATE_HOME:-~/.local/state}/ast-tool/plans` (o `AST_TOOL_STATE_DIR`), contienen bytes de código exactos y son privados. No copiar su contenido al chat si el repo es sensible.
|
|
91
|
+
|
|
92
|
+
## Seguridad que sí ofrece
|
|
93
|
+
|
|
94
|
+
- proyecto fresco al preparar mutaciones;
|
|
95
|
+
- diagnostics pre/post y bloqueo de errores nuevos por default;
|
|
96
|
+
- plan vinculado por hash al workspace completo, configs extendidos/referenciados y postimages;
|
|
97
|
+
- rechazo si cambió cualquier source/config antes de apply;
|
|
98
|
+
- lock filesystem cooperativo por config/workspace canónico compartido por MCP y CLI;
|
|
99
|
+
- staging, flush, rename, verificación de hash y rollback conservador;
|
|
100
|
+
- retry idempotente de un operation ya aplicado;
|
|
101
|
+
- preservación de modo y UTF-8 BOM.
|
|
102
|
+
|
|
103
|
+
La edición AST no demuestra que un cambio sea semánticamente correcto ni vuelve imposible romper código. La garantía práctica surge de combinar selección estructural, diagnostics, preview exacto, hash revisado, freshness checks y apply fail-closed.
|
|
104
|
+
|
|
105
|
+
## Límites
|
|
106
|
+
|
|
107
|
+
- No es una transacción global multiarchivo: la atomicidad depende del rename por archivo.
|
|
108
|
+
- El rollback es best effort y no pisa cambios de otro writer.
|
|
109
|
+
- El lock coordina superficies de apply que usan el mismo state directory; no coordina editores, NFS ni writers hostiles.
|
|
110
|
+
- Los planes MCP son in-memory y se pierden al reiniciar el servidor; los planes CLI son archivos privados, versionados y con el mismo TTL.
|
|
111
|
+
- El receipt se persiste dentro de la sección crítica. Si esa persistencia falla después de reemplazar sources, apply sale no-cero y el retry recupera sólo si el workspace completo coincide exactamente con el fingerprint post-apply revisado.
|
|
112
|
+
- Un crash duro puede dejar un lock stale: retirarlo requiere inspeccionar metadata y confirmar que no hay apply activo. Un estado parcial o divergente sigue fallando cerrado.
|
|
113
|
+
- Solo admite sources UTF-8 con o sin BOM.
|
|
114
|
+
- No implementa migración arbitraria de firmas, creación/eliminación de archivos ni lenguajes fuera de TS/JS.
|
|
115
|
+
- `allow_new_errors: true` evita el bloqueo del plan; no convierte código roto en código sano.
|
|
116
|
+
|
|
117
|
+
## Verificación
|
|
118
|
+
|
|
119
|
+
Después de aplicar, correr el gate canónico del repo. Como mínimo, un typecheck/build. Para cambios cross-file o con dinero/producción en juego, también la suite focalizada y la completa que corresponda.
|