ast-mcp-server 0.4.0 → 0.5.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/CHANGELOG.md +42 -0
- package/README.md +14 -6
- package/benchmark/README.md +18 -7
- package/dist/batch/runner.js +2 -2
- package/dist/batch/schema.js +5 -1
- package/dist/server.js +8 -1
- package/dist/services/agent-setup.js +1 -0
- package/dist/services/operation-plan-file.js +1 -1
- package/dist/services/operations.js +172 -17
- package/dist/services/pagination.js +16 -10
- package/dist/services/project-status.js +736 -0
- package/dist/services/project.js +87 -13
- package/dist/services/read-contracts.js +159 -0
- package/dist/services/scaffold.js +241 -0
- package/dist/services/symbols.js +16 -0
- package/dist/services/workspace.js +12 -6
- package/dist/tools/apply_operation.js +1 -1
- package/dist/tools/find_references.js +30 -7
- package/dist/tools/get_project_status.js +68 -0
- package/dist/tools/operation-schema.js +1 -1
- package/dist/tools/scaffold_class.js +145 -0
- package/dist/tools/search_symbols.js +77 -16
- package/package.json +2 -1
- package/skills/structural-code-editing/SKILL.md +15 -5
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { PaginationInputSchema, PaginationOutputSchema, paginate } from "../services/pagination.js";
|
|
5
5
|
import { findDeclarationByName, getSourceFileOrThrow, withProject } from "../services/project.js";
|
|
6
6
|
import { errorResult, formattedResult, ToolOutputFormatInputSchema } from "./result.js";
|
|
7
|
+
const ReferenceDetailSchema = z.enum(["locations", "context"]).default("locations");
|
|
7
8
|
const AstFindReferencesInputSchema = z.object({
|
|
8
9
|
project_root: z
|
|
9
10
|
.string()
|
|
@@ -11,10 +12,11 @@ const AstFindReferencesInputSchema = z.object({
|
|
|
11
12
|
file_path: z.string().describe("File containing the declaration."),
|
|
12
13
|
symbol_path: z.string().describe("Exact symbol path returned by an outline or symbol search."),
|
|
13
14
|
include_declaration: z.boolean().default(true),
|
|
15
|
+
detail: ReferenceDetailSchema.describe("Result detail: locations omits source lines; context adds bounded source-line text."),
|
|
14
16
|
...ToolOutputFormatInputSchema,
|
|
15
17
|
...PaginationInputSchema,
|
|
16
18
|
});
|
|
17
|
-
const
|
|
19
|
+
const ContextReferenceSchema = z.object({
|
|
18
20
|
file: z.string(),
|
|
19
21
|
line: z.number().int().positive(),
|
|
20
22
|
column: z.number().int().positive(),
|
|
@@ -22,14 +24,25 @@ const ReferenceSchema = z.object({
|
|
|
22
24
|
is_declaration: z.boolean(),
|
|
23
25
|
context: z.string(),
|
|
24
26
|
});
|
|
25
|
-
const
|
|
27
|
+
const LocationReferenceSchema = ContextReferenceSchema.omit({ context: true });
|
|
28
|
+
const ReferenceOutputBase = {
|
|
26
29
|
symbol: z.string(),
|
|
30
|
+
include_declaration: z.boolean(),
|
|
27
31
|
declaration_count: z.number().int().min(0),
|
|
28
32
|
reference_count: z.number().int().min(0),
|
|
29
33
|
affected_files: z.array(z.string()),
|
|
30
|
-
references: z.array(ReferenceSchema),
|
|
31
34
|
...PaginationOutputSchema,
|
|
32
|
-
}
|
|
35
|
+
};
|
|
36
|
+
const ReferenceOutputSchemas = {
|
|
37
|
+
locations: z.object({
|
|
38
|
+
...ReferenceOutputBase,
|
|
39
|
+
references: z.array(LocationReferenceSchema),
|
|
40
|
+
}),
|
|
41
|
+
context: z.object({
|
|
42
|
+
...ReferenceOutputBase,
|
|
43
|
+
references: z.array(ContextReferenceSchema),
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
33
46
|
export function registerFindReferences(server) {
|
|
34
47
|
server.registerTool("ast_find_references", {
|
|
35
48
|
title: "Find semantic symbol references",
|
|
@@ -41,7 +54,7 @@ export function registerFindReferences(server) {
|
|
|
41
54
|
idempotentHint: true,
|
|
42
55
|
openWorldHint: false,
|
|
43
56
|
},
|
|
44
|
-
}, async ({ project_root, file_path, symbol_path, include_declaration, output_format, offset, limit, }) => {
|
|
57
|
+
}, async ({ project_root, file_path, symbol_path, include_declaration, detail, output_format, offset, limit, }) => {
|
|
45
58
|
try {
|
|
46
59
|
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
47
60
|
const sourceFile = getSourceFileOrThrow(project, file_path);
|
|
@@ -81,14 +94,24 @@ export function registerFindReferences(server) {
|
|
|
81
94
|
const { items, ...metadata } = page;
|
|
82
95
|
return {
|
|
83
96
|
symbol: symbol_path,
|
|
97
|
+
include_declaration,
|
|
84
98
|
declaration_count: 1,
|
|
85
99
|
reference_count: references.length,
|
|
86
100
|
affected_files: affectedFiles,
|
|
87
|
-
references:
|
|
101
|
+
references: detail === "context"
|
|
102
|
+
? items
|
|
103
|
+
: items.map(({ file, line, column, kind, is_declaration }) => ({
|
|
104
|
+
file,
|
|
105
|
+
line,
|
|
106
|
+
column,
|
|
107
|
+
kind,
|
|
108
|
+
is_declaration,
|
|
109
|
+
})),
|
|
88
110
|
...metadata,
|
|
89
111
|
};
|
|
90
112
|
});
|
|
91
|
-
|
|
113
|
+
const outputSchema = ReferenceOutputSchemas[detail];
|
|
114
|
+
return formattedResult(outputSchema, structuredContent, output_format);
|
|
92
115
|
}
|
|
93
116
|
catch (error) {
|
|
94
117
|
return errorResult(error);
|
|
@@ -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
|
+
}
|
|
@@ -10,7 +10,7 @@ export const NormalizedDiagnosticSchema = z.object({
|
|
|
10
10
|
export const PreparedOperationOutputSchema = z.object({
|
|
11
11
|
operation_id: z.string().uuid(),
|
|
12
12
|
plan_hash: z.string().length(64),
|
|
13
|
-
kind: z.enum(["rename_symbol", "replace_symbol_body"]),
|
|
13
|
+
kind: z.enum(["rename_symbol", "replace_symbol_body", "scaffold_class"]),
|
|
14
14
|
status: z.literal("prepared"),
|
|
15
15
|
project_root: z.string(),
|
|
16
16
|
created_at: z.string(),
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { prepareScaffoldClass } from "../services/operations.js";
|
|
3
|
+
import { errorResult, structuredResult } from "./result.js";
|
|
4
|
+
import { PreparedOperationOutputSchema, serializePreparedOperation } from "./operation-schema.js";
|
|
5
|
+
const IdentifierSchema = z.string().min(1).max(200);
|
|
6
|
+
const TypeTextSchema = z.string().min(1).max(10_000);
|
|
7
|
+
const AccessSchema = z.enum(["public", "protected", "private"]);
|
|
8
|
+
const ConstructorAccessSchema = z.enum([
|
|
9
|
+
"public",
|
|
10
|
+
"protected",
|
|
11
|
+
"private",
|
|
12
|
+
"public readonly",
|
|
13
|
+
"protected readonly",
|
|
14
|
+
"private readonly",
|
|
15
|
+
]);
|
|
16
|
+
const ImportSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
from: z.string().min(1).max(500),
|
|
19
|
+
named: z.array(IdentifierSchema).max(100).default([]),
|
|
20
|
+
default: IdentifierSchema.optional(),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
const ParameterSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
name: IdentifierSchema,
|
|
26
|
+
type: TypeTextSchema,
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
const ConstructorParameterSchema = ParameterSchema.extend({
|
|
30
|
+
access_modifier: ConstructorAccessSchema,
|
|
31
|
+
}).strict();
|
|
32
|
+
const PropertySchema = z
|
|
33
|
+
.object({
|
|
34
|
+
name: IdentifierSchema,
|
|
35
|
+
type: TypeTextSchema,
|
|
36
|
+
access: AccessSchema.default("private"),
|
|
37
|
+
initializer: z.string().max(10_000).optional(),
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
const MethodSchema = z
|
|
41
|
+
.object({
|
|
42
|
+
name: IdentifierSchema,
|
|
43
|
+
is_async: z.boolean().default(false),
|
|
44
|
+
return_type: TypeTextSchema,
|
|
45
|
+
access: AccessSchema.default("public"),
|
|
46
|
+
parameters: z.array(ParameterSchema).max(100).default([]),
|
|
47
|
+
decorators: z.array(z.string().min(1).max(10_000)).max(50).default([]),
|
|
48
|
+
docs: z.array(z.string().min(1).max(10_000)).max(50).default([]),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
export const ScaffoldClassInputSchema = z
|
|
52
|
+
.object({
|
|
53
|
+
project_root: z.string().min(1),
|
|
54
|
+
file_path: z.string().min(1).max(1_000),
|
|
55
|
+
class_name: IdentifierSchema,
|
|
56
|
+
extends: TypeTextSchema.optional(),
|
|
57
|
+
implements: z.array(TypeTextSchema).max(50).default([]),
|
|
58
|
+
decorators: z.array(z.string().min(1).max(10_000)).max(50).default([]),
|
|
59
|
+
imports: z.array(ImportSchema).max(100).default([]),
|
|
60
|
+
constructor_params: z.array(ConstructorParameterSchema).max(100).default([]),
|
|
61
|
+
properties: z.array(PropertySchema).max(200).default([]),
|
|
62
|
+
methods: z.array(MethodSchema).min(1).max(200),
|
|
63
|
+
dry_run: z.literal(true).default(true),
|
|
64
|
+
allow_new_errors: z.boolean().default(false),
|
|
65
|
+
})
|
|
66
|
+
.strict();
|
|
67
|
+
const ScaffoldClassOutputSchema = PreparedOperationOutputSchema.extend({
|
|
68
|
+
file: z.string(),
|
|
69
|
+
class_name: z.string(),
|
|
70
|
+
outline: z.string(),
|
|
71
|
+
pending_methods: z.array(z.string()),
|
|
72
|
+
});
|
|
73
|
+
function toSpec(args) {
|
|
74
|
+
return {
|
|
75
|
+
className: args.class_name,
|
|
76
|
+
...(args.extends ? { extends: args.extends } : {}),
|
|
77
|
+
implements: args.implements,
|
|
78
|
+
decorators: args.decorators,
|
|
79
|
+
imports: args.imports.map((item) => ({
|
|
80
|
+
from: item.from,
|
|
81
|
+
named: item.named,
|
|
82
|
+
...(item.default ? { default: item.default } : {}),
|
|
83
|
+
})),
|
|
84
|
+
constructorParams: args.constructor_params.map((parameter) => ({
|
|
85
|
+
name: parameter.name,
|
|
86
|
+
type: parameter.type,
|
|
87
|
+
accessModifier: parameter.access_modifier,
|
|
88
|
+
})),
|
|
89
|
+
properties: args.properties.map((property) => ({
|
|
90
|
+
name: property.name,
|
|
91
|
+
type: property.type,
|
|
92
|
+
accessModifier: property.access,
|
|
93
|
+
...(property.initializer !== undefined ? { initializer: property.initializer } : {}),
|
|
94
|
+
})),
|
|
95
|
+
methods: args.methods.map((method) => ({
|
|
96
|
+
name: method.name,
|
|
97
|
+
isAsync: method.is_async,
|
|
98
|
+
returnType: method.return_type,
|
|
99
|
+
accessModifier: method.access,
|
|
100
|
+
params: method.parameters.map((parameter) => ({
|
|
101
|
+
name: parameter.name,
|
|
102
|
+
type: parameter.type,
|
|
103
|
+
})),
|
|
104
|
+
decorators: method.decorators,
|
|
105
|
+
docs: method.docs,
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function registerScaffoldClass(server) {
|
|
110
|
+
// The SDK compatibility overload exceeds TypeScript's instantiation depth for this schema.
|
|
111
|
+
// Keep the complete runtime schema and isolate only the registrar type.
|
|
112
|
+
const registerTool = server.registerTool.bind(server);
|
|
113
|
+
registerTool("ast_scaffold_class", {
|
|
114
|
+
title: "Prepare Class Scaffold",
|
|
115
|
+
description: "Prepares a hash-bound new TypeScript class file without writing it. Each method body throws an explicit not-implemented error. Review the diff, then apply the returned operation_id and plan_hash with ast_apply_operation.",
|
|
116
|
+
inputSchema: ScaffoldClassInputSchema,
|
|
117
|
+
outputSchema: ScaffoldClassOutputSchema.shape,
|
|
118
|
+
annotations: {
|
|
119
|
+
readOnlyHint: true,
|
|
120
|
+
destructiveHint: false,
|
|
121
|
+
idempotentHint: false,
|
|
122
|
+
openWorldHint: false,
|
|
123
|
+
},
|
|
124
|
+
}, async (args) => {
|
|
125
|
+
try {
|
|
126
|
+
const prepared = await prepareScaffoldClass({
|
|
127
|
+
projectRoot: args.project_root,
|
|
128
|
+
filePath: args.file_path,
|
|
129
|
+
spec: toSpec(args),
|
|
130
|
+
allowNewErrors: args.allow_new_errors,
|
|
131
|
+
});
|
|
132
|
+
const structuredContent = {
|
|
133
|
+
...serializePreparedOperation(prepared.operation),
|
|
134
|
+
file: prepared.file,
|
|
135
|
+
class_name: prepared.className,
|
|
136
|
+
outline: prepared.outline,
|
|
137
|
+
pending_methods: prepared.pendingMethods,
|
|
138
|
+
};
|
|
139
|
+
return structuredResult(structuredContent);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return errorResult(error);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { buildFileOutline } from "../services/outline.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createPaginationInputSchema, PaginationOutputSchema, paginate, } from "../services/pagination.js";
|
|
5
5
|
import { withProject } from "../services/project.js";
|
|
6
|
-
import { collectSymbols } from "../services/symbols.js";
|
|
6
|
+
import { collectSymbols, symbolMatchRank } from "../services/symbols.js";
|
|
7
7
|
import { errorResult, formattedResult, ToolOutputFormatInputSchema } from "./result.js";
|
|
8
|
+
const SEARCH_DEFAULT_LIMIT = 20;
|
|
9
|
+
const SearchDetailSchema = z.enum(["selectors", "summary", "full"]).default("summary");
|
|
8
10
|
const AstSearchSymbolsInputSchema = z.object({
|
|
9
11
|
project_root: z
|
|
10
12
|
.string()
|
|
@@ -21,10 +23,11 @@ const AstSearchSymbolsInputSchema = z.object({
|
|
|
21
23
|
.string()
|
|
22
24
|
.optional()
|
|
23
25
|
.describe("Optional case-insensitive substring matched against project-relative file paths."),
|
|
26
|
+
detail: SearchDetailSchema.describe("Result detail: selectors for routing, summary adds signatures, full adds redundant declaration fields."),
|
|
24
27
|
...ToolOutputFormatInputSchema,
|
|
25
|
-
...
|
|
28
|
+
...createPaginationInputSchema(SEARCH_DEFAULT_LIMIT),
|
|
26
29
|
});
|
|
27
|
-
const
|
|
30
|
+
const FullSearchSymbolSchema = z.object({
|
|
28
31
|
file: z.string(),
|
|
29
32
|
symbol_path: z.string(),
|
|
30
33
|
selector: z.string(),
|
|
@@ -33,15 +36,51 @@ const SearchSymbolSchema = z.object({
|
|
|
33
36
|
line: z.number().int().positive(),
|
|
34
37
|
signature: z.string(),
|
|
35
38
|
});
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const SelectorSearchSymbolSchema = FullSearchSymbolSchema.pick({
|
|
40
|
+
file: true,
|
|
41
|
+
selector: true,
|
|
42
|
+
kind: true,
|
|
40
43
|
});
|
|
44
|
+
const SummarySearchSymbolSchema = FullSearchSymbolSchema.pick({
|
|
45
|
+
file: true,
|
|
46
|
+
selector: true,
|
|
47
|
+
kind: true,
|
|
48
|
+
signature: true,
|
|
49
|
+
});
|
|
50
|
+
const SearchOutputSchemas = {
|
|
51
|
+
selectors: z.object({
|
|
52
|
+
symbols: z.array(SelectorSearchSymbolSchema),
|
|
53
|
+
duration_ms: z.number().min(0),
|
|
54
|
+
...PaginationOutputSchema,
|
|
55
|
+
}),
|
|
56
|
+
summary: z.object({
|
|
57
|
+
symbols: z.array(SummarySearchSymbolSchema),
|
|
58
|
+
duration_ms: z.number().min(0),
|
|
59
|
+
...PaginationOutputSchema,
|
|
60
|
+
}),
|
|
61
|
+
full: z.object({
|
|
62
|
+
symbols: z.array(FullSearchSymbolSchema),
|
|
63
|
+
duration_ms: z.number().min(0),
|
|
64
|
+
...PaginationOutputSchema,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
function projectSymbols(detail, symbols) {
|
|
68
|
+
if (detail === "full")
|
|
69
|
+
return symbols;
|
|
70
|
+
if (detail === "summary") {
|
|
71
|
+
return symbols.map(({ file, selector, kind, signature }) => ({
|
|
72
|
+
file,
|
|
73
|
+
selector,
|
|
74
|
+
kind,
|
|
75
|
+
signature,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
return symbols.map(({ file, selector, kind }) => ({ file, selector, kind }));
|
|
79
|
+
}
|
|
41
80
|
export function registerSearchSymbols(server) {
|
|
42
81
|
server.registerTool("ast_search_symbols", {
|
|
43
82
|
title: "Search project symbols",
|
|
44
|
-
description: "
|
|
83
|
+
description: "Ranks structural declarations and returns exact file/symbol selectors for downstream AST tools.",
|
|
45
84
|
inputSchema: AstSearchSymbolsInputSchema,
|
|
46
85
|
annotations: {
|
|
47
86
|
readOnlyHint: true,
|
|
@@ -49,7 +88,7 @@ export function registerSearchSymbols(server) {
|
|
|
49
88
|
idempotentHint: true,
|
|
50
89
|
openWorldHint: false,
|
|
51
90
|
},
|
|
52
|
-
}, async ({ project_root, query, kinds, file_filter, output_format, offset, limit }) => {
|
|
91
|
+
}, async ({ project_root, query, kinds, file_filter, detail, output_format, offset, limit }) => {
|
|
53
92
|
try {
|
|
54
93
|
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
55
94
|
const startedAt = performance.now();
|
|
@@ -62,9 +101,13 @@ export function registerSearchSymbols(server) {
|
|
|
62
101
|
if (normalizedFileFilter && !file.toLowerCase().includes(normalizedFileFilter)) {
|
|
63
102
|
continue;
|
|
64
103
|
}
|
|
65
|
-
const matchingSymbols = collectSymbols(sourceFile).filter((symbol) =>
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
});
|
|
68
111
|
if (matchingSymbols.length === 0)
|
|
69
112
|
continue;
|
|
70
113
|
const signatures = new Map(buildFileOutline(sourceFile).symbols.map((symbol) => [
|
|
@@ -83,16 +126,34 @@ export function registerSearchSymbols(server) {
|
|
|
83
126
|
});
|
|
84
127
|
}
|
|
85
128
|
}
|
|
86
|
-
matches.sort((left, right) =>
|
|
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));
|
|
146
|
+
});
|
|
87
147
|
const page = paginate(matches, offset, limit);
|
|
88
148
|
const { items, ...metadata } = page;
|
|
89
149
|
return {
|
|
90
|
-
symbols: items,
|
|
150
|
+
symbols: projectSymbols(detail, items),
|
|
91
151
|
duration_ms: performance.now() - startedAt,
|
|
92
152
|
...metadata,
|
|
93
153
|
};
|
|
94
154
|
});
|
|
95
|
-
|
|
155
|
+
const outputSchema = SearchOutputSchemas[detail];
|
|
156
|
+
return formattedResult(outputSchema, structuredContent, output_format);
|
|
96
157
|
}
|
|
97
158
|
catch (error) {
|
|
98
159
|
return errorResult(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ast-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"benchmark:corpus": "yarn build && node scripts/benchmark-corpus.mjs",
|
|
56
56
|
"benchmark:batch": "yarn build && node scripts/benchmark-batch.mjs",
|
|
57
57
|
"benchmark:formats": "yarn build && node scripts/benchmark-formats.mjs",
|
|
58
|
+
"benchmark:shapes": "yarn build && node scripts/benchmark-result-shapes.mjs",
|
|
58
59
|
"lint": "eslint src test scripts vitest.config.ts eslint.config.js",
|
|
59
60
|
"format": "prettier --write .",
|
|
60
61
|
"format:check": "prettier --check .",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: structural-code-editing
|
|
3
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: "
|
|
4
|
+
version: "4.0.0"
|
|
5
5
|
author: "yail"
|
|
6
6
|
license: "ISC"
|
|
7
7
|
metadata:
|
|
@@ -21,6 +21,7 @@ Usar las tools del servidor MCP `ast` en proyectos TypeScript/JavaScript con `ts
|
|
|
21
21
|
- medir el radio de impacto de un rename;
|
|
22
22
|
- renombrar un símbolo en varios archivos;
|
|
23
23
|
- reemplazar solo el cuerpo de una función, método, accessor o callable property;
|
|
24
|
+
- crear un archivo de clase nuevo mediante un scaffold estructurado y revisado;
|
|
24
25
|
- obtener diagnostics sin cargar el proyecto como texto en el contexto.
|
|
25
26
|
|
|
26
27
|
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í.
|
|
@@ -45,6 +46,8 @@ El setup falla cerrado ante un registro `ast` conflictivo. No eliminar ni reempl
|
|
|
45
46
|
5. `ast_find_references` antes de renames o cambios con impacto cross-file.
|
|
46
47
|
6. `ast_get_diagnostics` para establecer y verificar el estado del proyecto.
|
|
47
48
|
|
|
49
|
+
`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
|
+
|
|
48
51
|
`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
52
|
|
|
50
53
|
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.
|
|
@@ -77,9 +80,9 @@ Cuando un pipeline conocido requiere varias llamadas MCP dependientes y el clien
|
|
|
77
80
|
|
|
78
81
|
## Flujo obligatorio de mutación
|
|
79
82
|
|
|
80
|
-
Las tools de rename y
|
|
83
|
+
Las tools de rename, reemplazo y scaffold **solo preparan**. `dry_run: false` directo está deshabilitado.
|
|
81
84
|
|
|
82
|
-
1. Llamar `ast_rename_symbol` o `
|
|
85
|
+
1. Llamar `ast_rename_symbol`, `ast_replace_symbol_body` o `ast_scaffold_class` con `dry_run: true`.
|
|
83
86
|
2. Revisar:
|
|
84
87
|
- `affected_files`;
|
|
85
88
|
- summaries y previews;
|
|
@@ -93,6 +96,12 @@ Las tools de rename y reemplazo **solo preparan**. `dry_run: false` directo est
|
|
|
93
96
|
|
|
94
97
|
Nunca reconstruir el contenido a aplicar a partir del diff. Apply escribe los postimages exactos retenidos en el plan.
|
|
95
98
|
|
|
99
|
+
### Scaffold de clase
|
|
100
|
+
|
|
101
|
+
Usar `ast_scaffold_class` solo para crear un `.ts`/`.tsx` ausente con una clase estructurada. Pasar imports, heritage, decorators, constructor params, properties inicializadas y métodos mediante el schema; no inyectar un archivo completo como string. Debe haber al menos un método.
|
|
102
|
+
|
|
103
|
+
Cada método generado contiene exclusivamente `throw new Error("Not implemented: Class.method")`. Preparar el scaffold, revisar el diff desde `/dev/null`, diagnostics, `pending_methods`, `blocked` y `plan_hash`, aplicar y luego reemplazar cada selector pendiente con `ast_replace_symbol_body`. Un target existente, path fuera del proyecto o parent simbólico debe fallar; no buscar un fallback textual.
|
|
104
|
+
|
|
96
105
|
### Mutaciones desde el CLI
|
|
97
106
|
|
|
98
107
|
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:
|
|
@@ -110,7 +119,8 @@ Los planes CLI sobreviven al proceso bajo `${XDG_STATE_HOME:-~/.local/state}/ast
|
|
|
110
119
|
- plan vinculado por hash al workspace completo, configs extendidos/referenciados y postimages;
|
|
111
120
|
- rechazo si cambió cualquier source/config antes de apply;
|
|
112
121
|
- lock filesystem cooperativo por config/workspace canónico compartido por MCP y CLI;
|
|
113
|
-
- staging, flush, rename, verificación de hash y rollback conservador;
|
|
122
|
+
- staging, flush, rename para reemplazos y creación no-clobber para targets nuevos, con verificación de hash y rollback conservador;
|
|
123
|
+
- scaffold create-only con target ausente, parent real, postimage incluida en el fingerprint y diff contra `/dev/null`;
|
|
114
124
|
- retry idempotente de un operation ya aplicado;
|
|
115
125
|
- preservación de modo y UTF-8 BOM.
|
|
116
126
|
|
|
@@ -125,7 +135,7 @@ La edición AST no demuestra que un cambio sea semánticamente correcto ni vuelv
|
|
|
125
135
|
- 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.
|
|
126
136
|
- 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.
|
|
127
137
|
- Solo admite sources UTF-8 con o sin BOM.
|
|
128
|
-
- No implementa migración arbitraria de firmas, creación/eliminación de archivos ni lenguajes fuera de TS/JS.
|
|
138
|
+
- No implementa migración arbitraria de firmas, creación/eliminación general de archivos ni lenguajes fuera de TS/JS; la única creación admitida es el scaffold de una clase.
|
|
129
139
|
- `allow_new_errors: true` evita el bloqueo del plan; no convierte código roto en código sano.
|
|
130
140
|
|
|
131
141
|
## Verificación
|