ast-mcp-server 0.3.0 → 0.5.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 +44 -0
- package/README.md +28 -8
- package/dist/batch/runner.js +10 -2
- package/dist/batch/schema.js +8 -1
- package/dist/cli-output.js +28 -0
- package/dist/cli.js +57 -9
- package/dist/server.js +6 -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/scaffold.js +241 -0
- package/dist/services/symbols.js +16 -0
- package/dist/tools/apply_operation.js +1 -1
- package/dist/tools/find_references.js +32 -9
- package/dist/tools/get_diagnostics.js +4 -4
- package/dist/tools/operation-schema.js +1 -1
- package/dist/tools/result.js +45 -1
- package/dist/tools/scaffold_class.js +145 -0
- package/dist/tools/search_symbols.js +79 -18
- package/package.json +9 -2
- package/skills/structural-code-editing/SKILL.md +30 -6
- package/benchmark/README.md +0 -86
|
@@ -3,7 +3,8 @@ import { Node } from "ts-morph";
|
|
|
3
3
|
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
|
-
import { errorResult,
|
|
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,9 +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."),
|
|
16
|
+
...ToolOutputFormatInputSchema,
|
|
14
17
|
...PaginationInputSchema,
|
|
15
18
|
});
|
|
16
|
-
const
|
|
19
|
+
const ContextReferenceSchema = z.object({
|
|
17
20
|
file: z.string(),
|
|
18
21
|
line: z.number().int().positive(),
|
|
19
22
|
column: z.number().int().positive(),
|
|
@@ -21,27 +24,37 @@ const ReferenceSchema = z.object({
|
|
|
21
24
|
is_declaration: z.boolean(),
|
|
22
25
|
context: z.string(),
|
|
23
26
|
});
|
|
24
|
-
const
|
|
27
|
+
const LocationReferenceSchema = ContextReferenceSchema.omit({ context: true });
|
|
28
|
+
const ReferenceOutputBase = {
|
|
25
29
|
symbol: z.string(),
|
|
30
|
+
include_declaration: z.boolean(),
|
|
26
31
|
declaration_count: z.number().int().min(0),
|
|
27
32
|
reference_count: z.number().int().min(0),
|
|
28
33
|
affected_files: z.array(z.string()),
|
|
29
|
-
references: z.array(ReferenceSchema),
|
|
30
34
|
...PaginationOutputSchema,
|
|
31
|
-
}
|
|
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
|
+
};
|
|
32
46
|
export function registerFindReferences(server) {
|
|
33
47
|
server.registerTool("ast_find_references", {
|
|
34
48
|
title: "Find semantic symbol references",
|
|
35
49
|
description: "Finds type-resolved references and returns bounded project-relative locations, including declaration impact by default.",
|
|
36
50
|
inputSchema: AstFindReferencesInputSchema,
|
|
37
|
-
outputSchema: AstFindReferencesOutputSchema,
|
|
38
51
|
annotations: {
|
|
39
52
|
readOnlyHint: true,
|
|
40
53
|
destructiveHint: false,
|
|
41
54
|
idempotentHint: true,
|
|
42
55
|
openWorldHint: false,
|
|
43
56
|
},
|
|
44
|
-
}, async ({ project_root, file_path, symbol_path, include_declaration, 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);
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { normalizeDiagnostic } from "../services/diagnostics.js";
|
|
3
3
|
import { PaginationInputSchema, PaginationOutputSchema, paginate } from "../services/pagination.js";
|
|
4
4
|
import { getSourceFileOrThrow, withProject } from "../services/project.js";
|
|
5
|
-
import { errorResult,
|
|
5
|
+
import { errorResult, formattedResult, ToolOutputFormatInputSchema } from "./result.js";
|
|
6
6
|
const DiagnosticSchema = z.object({
|
|
7
7
|
code: z.number().int(),
|
|
8
8
|
category: z.string(),
|
|
@@ -19,6 +19,7 @@ const AstGetDiagnosticsInputSchema = z.object({
|
|
|
19
19
|
.string()
|
|
20
20
|
.optional()
|
|
21
21
|
.describe("Optional project-relative or absolute file path. Omit for project diagnostics."),
|
|
22
|
+
...ToolOutputFormatInputSchema,
|
|
22
23
|
...PaginationInputSchema,
|
|
23
24
|
});
|
|
24
25
|
const AstGetDiagnosticsOutputSchema = z.object({
|
|
@@ -33,14 +34,13 @@ export function registerGetDiagnostics(server) {
|
|
|
33
34
|
title: "Get TypeScript diagnostics",
|
|
34
35
|
description: "Returns bounded normalized TypeScript diagnostics for a project or one source file. Existing errors are preserved as evidence for write-operation delta checks.",
|
|
35
36
|
inputSchema: AstGetDiagnosticsInputSchema,
|
|
36
|
-
outputSchema: AstGetDiagnosticsOutputSchema,
|
|
37
37
|
annotations: {
|
|
38
38
|
readOnlyHint: true,
|
|
39
39
|
destructiveHint: false,
|
|
40
40
|
idempotentHint: true,
|
|
41
41
|
openWorldHint: false,
|
|
42
42
|
},
|
|
43
|
-
}, async ({ project_root, file_path, offset, limit }) => {
|
|
43
|
+
}, async ({ project_root, file_path, output_format, offset, limit }) => {
|
|
44
44
|
try {
|
|
45
45
|
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
46
46
|
const startedAt = performance.now();
|
|
@@ -61,7 +61,7 @@ export function registerGetDiagnostics(server) {
|
|
|
61
61
|
...metadata,
|
|
62
62
|
};
|
|
63
63
|
});
|
|
64
|
-
return
|
|
64
|
+
return formattedResult(AstGetDiagnosticsOutputSchema, structuredContent, output_format);
|
|
65
65
|
}
|
|
66
66
|
catch (error) {
|
|
67
67
|
return errorResult(error);
|
|
@@ -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(),
|
package/dist/tools/result.js
CHANGED
|
@@ -1,12 +1,56 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { decode, encode } from "@toon-format/toon";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
export const MAX_TOOL_ERROR_BYTES = 64 * 1024;
|
|
6
|
+
export const MAX_TOON_RESULT_BYTES = 10 * 1024 * 1024;
|
|
7
|
+
export const ToolOutputFormatSchema = z.enum(["json", "toon"]).default("json");
|
|
8
|
+
export const ToolOutputFormatInputSchema = {
|
|
9
|
+
output_format: ToolOutputFormatSchema.describe("Result representation. TOON wraps one compact encoded value in structuredContent."),
|
|
10
|
+
};
|
|
11
|
+
export const ToonResultSchema = z.object({
|
|
12
|
+
format: z.literal("toon"),
|
|
13
|
+
data: z.string(),
|
|
14
|
+
});
|
|
1
15
|
export function structuredResult(structuredContent) {
|
|
2
16
|
return { content: [], structuredContent };
|
|
3
17
|
}
|
|
18
|
+
export function formattedResult(outputSchema, candidate, outputFormat, maxToonBytes = MAX_TOON_RESULT_BYTES) {
|
|
19
|
+
const validated = outputSchema.parse(candidate);
|
|
20
|
+
if (outputFormat === "json") {
|
|
21
|
+
return structuredResult(validated);
|
|
22
|
+
}
|
|
23
|
+
const text = encode(validated);
|
|
24
|
+
const byteLength = Buffer.byteLength(text, "utf8");
|
|
25
|
+
if (byteLength > maxToonBytes) {
|
|
26
|
+
throw new Error(`TOON result exceeds the ${maxToonBytes}-byte limit (${byteLength} bytes encoded).`);
|
|
27
|
+
}
|
|
28
|
+
if (!isDeepStrictEqual(decode(text), validated)) {
|
|
29
|
+
throw new Error("TOON result cannot represent the validated value losslessly.");
|
|
30
|
+
}
|
|
31
|
+
return structuredResult({ format: "toon", data: text });
|
|
32
|
+
}
|
|
33
|
+
function truncateUtf8(value, maxBytes) {
|
|
34
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
35
|
+
return value;
|
|
36
|
+
const suffix = "... [truncated]";
|
|
37
|
+
const source = Buffer.from(value, "utf8");
|
|
38
|
+
let end = Math.max(0, maxBytes - Buffer.byteLength(suffix, "utf8"));
|
|
39
|
+
while (end > 0 && (source[end] & 0xc0) === 0x80)
|
|
40
|
+
end -= 1;
|
|
41
|
+
let prefix = source.subarray(0, end).toString("utf8");
|
|
42
|
+
while (Buffer.byteLength(prefix + suffix, "utf8") > maxBytes) {
|
|
43
|
+
prefix = prefix.slice(0, -1);
|
|
44
|
+
}
|
|
45
|
+
return prefix + suffix;
|
|
46
|
+
}
|
|
4
47
|
export function errorResult(error) {
|
|
48
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5
49
|
return {
|
|
6
50
|
content: [
|
|
7
51
|
{
|
|
8
52
|
type: "text",
|
|
9
|
-
text:
|
|
53
|
+
text: truncateUtf8(message, MAX_TOOL_ERROR_BYTES),
|
|
10
54
|
},
|
|
11
55
|
],
|
|
12
56
|
isError: true,
|
|
@@ -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";
|
|
7
|
-
import { errorResult,
|
|
6
|
+
import { collectSymbols, symbolMatchRank } from "../services/symbols.js";
|
|
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,9 +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."),
|
|
24
|
-
|
|
26
|
+
detail: SearchDetailSchema.describe("Result detail: selectors for routing, summary adds signatures, full adds redundant declaration fields."),
|
|
27
|
+
...ToolOutputFormatInputSchema,
|
|
28
|
+
...createPaginationInputSchema(SEARCH_DEFAULT_LIMIT),
|
|
25
29
|
});
|
|
26
|
-
const
|
|
30
|
+
const FullSearchSymbolSchema = z.object({
|
|
27
31
|
file: z.string(),
|
|
28
32
|
symbol_path: z.string(),
|
|
29
33
|
selector: z.string(),
|
|
@@ -32,24 +36,59 @@ const SearchSymbolSchema = z.object({
|
|
|
32
36
|
line: z.number().int().positive(),
|
|
33
37
|
signature: z.string(),
|
|
34
38
|
});
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
const SelectorSearchSymbolSchema = FullSearchSymbolSchema.pick({
|
|
40
|
+
file: true,
|
|
41
|
+
selector: true,
|
|
42
|
+
kind: true,
|
|
39
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
|
+
}
|
|
40
80
|
export function registerSearchSymbols(server) {
|
|
41
81
|
server.registerTool("ast_search_symbols", {
|
|
42
82
|
title: "Search project symbols",
|
|
43
|
-
description: "
|
|
83
|
+
description: "Ranks structural declarations and returns exact file/symbol selectors for downstream AST tools.",
|
|
44
84
|
inputSchema: AstSearchSymbolsInputSchema,
|
|
45
|
-
outputSchema: AstSearchSymbolsOutputSchema,
|
|
46
85
|
annotations: {
|
|
47
86
|
readOnlyHint: true,
|
|
48
87
|
destructiveHint: false,
|
|
49
88
|
idempotentHint: true,
|
|
50
89
|
openWorldHint: false,
|
|
51
90
|
},
|
|
52
|
-
}, async ({ project_root, query, kinds, file_filter, 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.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",
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=20.19"
|
|
42
42
|
},
|
|
43
|
+
"resolutions": {
|
|
44
|
+
"hono": "4.12.34"
|
|
45
|
+
},
|
|
43
46
|
"scripts": {
|
|
44
47
|
"build": "tsc",
|
|
45
48
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
@@ -51,6 +54,8 @@
|
|
|
51
54
|
"benchmark": "yarn build && node scripts/benchmark.mjs",
|
|
52
55
|
"benchmark:corpus": "yarn build && node scripts/benchmark-corpus.mjs",
|
|
53
56
|
"benchmark:batch": "yarn build && node scripts/benchmark-batch.mjs",
|
|
57
|
+
"benchmark:formats": "yarn build && node scripts/benchmark-formats.mjs",
|
|
58
|
+
"benchmark:shapes": "yarn build && node scripts/benchmark-result-shapes.mjs",
|
|
54
59
|
"lint": "eslint src test scripts vitest.config.ts eslint.config.js",
|
|
55
60
|
"format": "prettier --write .",
|
|
56
61
|
"format:check": "prettier --check .",
|
|
@@ -62,6 +67,7 @@
|
|
|
62
67
|
},
|
|
63
68
|
"dependencies": {
|
|
64
69
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
70
|
+
"@toon-format/toon": "4.1.0",
|
|
65
71
|
"diff": "^9.0.0",
|
|
66
72
|
"ts-morph": "24.0.0",
|
|
67
73
|
"zod": "^3.23.8"
|
|
@@ -70,9 +76,10 @@
|
|
|
70
76
|
"@eslint/js": "^10.0.1",
|
|
71
77
|
"@types/node": "^22.5.0",
|
|
72
78
|
"eslint": "^10.8.0",
|
|
79
|
+
"gpt-tokenizer": "3.4.0",
|
|
73
80
|
"prettier": "^3.9.6",
|
|
74
81
|
"typescript": "^5.6.0",
|
|
75
82
|
"typescript-eslint": "^8.65.0",
|
|
76
83
|
"vitest": "^4.1.10"
|
|
77
84
|
}
|
|
78
|
-
}
|
|
85
|
+
}
|
|
@@ -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,10 +46,26 @@ 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.
|
|
51
54
|
|
|
55
|
+
## Formato de resultado para el modelo
|
|
56
|
+
|
|
57
|
+
`ast_search_symbols`, `ast_find_references` y `ast_get_diagnostics` aceptan `output_format: "toon"`. Usarlo cuando la respuesta vaya directo al modelo y se espere una colección uniforme con varios registros. Omitirlo, o usar `json`, para automatización que dependa del objeto canónico.
|
|
58
|
+
|
|
59
|
+
En MCP, TOON llega como un único envelope estructurado `{ "format": "toon", "data": "..." }`; `data` contiene el documento TOON lossless. No hay una copia JSON completa en paralelo. Las tres tools validan primero el valor canónico con Zod, pero no publican un `outputSchema` MCP único porque tienen dos representaciones de éxito.
|
|
60
|
+
|
|
61
|
+
No pedir TOON para `ast_list_files`, outlines, source, previews ni mutaciones: los benchmarks muestran que el envelope empeora esas formas pequeñas, multiline o diff-heavy. Para un pipeline read-only conocido, se puede compactar únicamente el resultado final con:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
ast-tool run pipeline.json --output-format toon
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Los pasos internos del batch siempre son JSON estructurado. TOON dentro de `step.input` y TOON final para un batch de preparación se rechazan antes de ejecutar la operación.
|
|
68
|
+
|
|
52
69
|
## Batch CLI para clientes con Bash
|
|
53
70
|
|
|
54
71
|
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.
|
|
@@ -59,13 +76,13 @@ Cuando un pipeline conocido requiere varias llamadas MCP dependientes y el clien
|
|
|
59
76
|
- Mantener paginación y filtros: batch elimina roundtrips, no vuelve razonable leer un monorepo entero.
|
|
60
77
|
- No generar JavaScript/eval dentro del documento; el contrato es declarativo y limitado.
|
|
61
78
|
|
|
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.
|
|
79
|
+
`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. Los errores del CLI permanecen como JSON en stderr aunque el output exitoso solicitado sea TOON.
|
|
63
80
|
|
|
64
81
|
## Flujo obligatorio de mutación
|
|
65
82
|
|
|
66
|
-
Las tools de rename y
|
|
83
|
+
Las tools de rename, reemplazo y scaffold **solo preparan**. `dry_run: false` directo está deshabilitado.
|
|
67
84
|
|
|
68
|
-
1. Llamar `ast_rename_symbol` o `
|
|
85
|
+
1. Llamar `ast_rename_symbol`, `ast_replace_symbol_body` o `ast_scaffold_class` con `dry_run: true`.
|
|
69
86
|
2. Revisar:
|
|
70
87
|
- `affected_files`;
|
|
71
88
|
- summaries y previews;
|
|
@@ -79,6 +96,12 @@ Las tools de rename y reemplazo **solo preparan**. `dry_run: false` directo est
|
|
|
79
96
|
|
|
80
97
|
Nunca reconstruir el contenido a aplicar a partir del diff. Apply escribe los postimages exactos retenidos en el plan.
|
|
81
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
|
+
|
|
82
105
|
### Mutaciones desde el CLI
|
|
83
106
|
|
|
84
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:
|
|
@@ -96,7 +119,8 @@ Los planes CLI sobreviven al proceso bajo `${XDG_STATE_HOME:-~/.local/state}/ast
|
|
|
96
119
|
- plan vinculado por hash al workspace completo, configs extendidos/referenciados y postimages;
|
|
97
120
|
- rechazo si cambió cualquier source/config antes de apply;
|
|
98
121
|
- lock filesystem cooperativo por config/workspace canónico compartido por MCP y CLI;
|
|
99
|
-
- 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`;
|
|
100
124
|
- retry idempotente de un operation ya aplicado;
|
|
101
125
|
- preservación de modo y UTF-8 BOM.
|
|
102
126
|
|
|
@@ -111,7 +135,7 @@ La edición AST no demuestra que un cambio sea semánticamente correcto ni vuelv
|
|
|
111
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.
|
|
112
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.
|
|
113
137
|
- 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.
|
|
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.
|
|
115
139
|
- `allow_new_errors: true` evita el bloqueo del plan; no convierte código roto en código sano.
|
|
116
140
|
|
|
117
141
|
## Verificación
|