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,127 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { link, mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const SKILL_NAME = "structural-code-editing";
|
|
6
|
+
export class SkillConflictError extends Error {
|
|
7
|
+
destination;
|
|
8
|
+
constructor(destination) {
|
|
9
|
+
super(`Skill already exists with different content at ${destination}; pass --force to replace it.`);
|
|
10
|
+
this.name = "SkillConflictError";
|
|
11
|
+
this.destination = destination;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function resolveBundledSkillPath(executablePath) {
|
|
15
|
+
const resolvedExecutable = await realpath(executablePath);
|
|
16
|
+
return path.resolve(path.dirname(resolvedExecutable), "..", "skills", SKILL_NAME, "SKILL.md");
|
|
17
|
+
}
|
|
18
|
+
function resolveConfiguredDirectory(configured, fallback, workingDirectory) {
|
|
19
|
+
return path.resolve(workingDirectory, configured ?? fallback);
|
|
20
|
+
}
|
|
21
|
+
async function resolveDestinationPlans(options) {
|
|
22
|
+
if (options.scope === "project" && options.target !== "claude") {
|
|
23
|
+
throw new Error("Project scope is supported only for Claude Code skills.");
|
|
24
|
+
}
|
|
25
|
+
const homeDirectory = options.homeDirectory ?? os.homedir();
|
|
26
|
+
const workingDirectory = options.workingDirectory ?? process.cwd();
|
|
27
|
+
const environment = options.environment ?? process.env;
|
|
28
|
+
const targets = options.target === "all" ? ["claude", "hermes"] : [options.target];
|
|
29
|
+
let projectRoot;
|
|
30
|
+
if (options.scope === "project") {
|
|
31
|
+
projectRoot = path.resolve(workingDirectory, options.projectRoot ?? workingDirectory);
|
|
32
|
+
const projectStats = await stat(projectRoot).catch((error) => {
|
|
33
|
+
if (error.code === "ENOENT") {
|
|
34
|
+
throw new Error(`Project root does not exist: ${projectRoot}`);
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
});
|
|
38
|
+
if (!projectStats.isDirectory()) {
|
|
39
|
+
throw new Error(`Project root is not a directory: ${projectRoot}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return targets.map((target) => {
|
|
43
|
+
if (target === "claude") {
|
|
44
|
+
const root = options.scope === "project"
|
|
45
|
+
? path.join(projectRoot, ".claude")
|
|
46
|
+
: resolveConfiguredDirectory(environment.CLAUDE_CONFIG_DIR, path.join(homeDirectory, ".claude"), workingDirectory);
|
|
47
|
+
return {
|
|
48
|
+
target,
|
|
49
|
+
scope: options.scope,
|
|
50
|
+
path: path.join(root, "skills", SKILL_NAME, "SKILL.md"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const hermesHome = resolveConfiguredDirectory(environment.HERMES_HOME, path.join(homeDirectory, ".hermes"), workingDirectory);
|
|
54
|
+
return {
|
|
55
|
+
target,
|
|
56
|
+
scope: "user",
|
|
57
|
+
path: path.join(hermesHome, "skills", "software-development", SKILL_NAME, "SKILL.md"),
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function classifyDestination(destination, sourceContent, force) {
|
|
62
|
+
try {
|
|
63
|
+
const existingContent = await readFile(destination, "utf8");
|
|
64
|
+
if (existingContent === sourceContent)
|
|
65
|
+
return "unchanged";
|
|
66
|
+
if (!force) {
|
|
67
|
+
throw new SkillConflictError(destination);
|
|
68
|
+
}
|
|
69
|
+
return "updated";
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.code === "ENOENT")
|
|
73
|
+
return "installed";
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function writeSkill(destination, sourceContent, status) {
|
|
78
|
+
if (status === "unchanged")
|
|
79
|
+
return;
|
|
80
|
+
const directory = path.dirname(destination);
|
|
81
|
+
await mkdir(directory, { recursive: true });
|
|
82
|
+
const temporaryPath = path.join(directory, `.${path.basename(destination)}.${process.pid}.${randomUUID()}.tmp`);
|
|
83
|
+
await writeFile(temporaryPath, sourceContent, {
|
|
84
|
+
encoding: "utf8",
|
|
85
|
+
flag: "wx",
|
|
86
|
+
mode: 0o644,
|
|
87
|
+
});
|
|
88
|
+
try {
|
|
89
|
+
if (status === "installed") {
|
|
90
|
+
await link(temporaryPath, destination).catch((error) => {
|
|
91
|
+
if (error.code === "EEXIST") {
|
|
92
|
+
throw new Error(`Skill appeared concurrently at ${destination}; no files were replaced.`);
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
});
|
|
96
|
+
await rm(temporaryPath);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await rename(temporaryPath, destination);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await rm(temporaryPath, { force: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function installBundledSkill(options) {
|
|
106
|
+
const sourceContent = await readFile(path.resolve(options.sourceSkillPath), "utf8");
|
|
107
|
+
const destinations = await resolveDestinationPlans(options);
|
|
108
|
+
const force = options.force ?? false;
|
|
109
|
+
const statusByPath = new Map();
|
|
110
|
+
for (const destination of destinations) {
|
|
111
|
+
if (!statusByPath.has(destination.path)) {
|
|
112
|
+
statusByPath.set(destination.path, await classifyDestination(destination.path, sourceContent, force));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const [destination, status] of statusByPath) {
|
|
116
|
+
await writeSkill(destination, sourceContent, status);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
version: 1,
|
|
120
|
+
status: "ok",
|
|
121
|
+
skill: SKILL_NAME,
|
|
122
|
+
installations: destinations.map((destination) => ({
|
|
123
|
+
...destination,
|
|
124
|
+
status: statusByPath.get(destination.path),
|
|
125
|
+
})),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Node, } from "ts-morph";
|
|
2
|
+
function namedNode(node) {
|
|
3
|
+
if (Node.isConstructorDeclaration(node))
|
|
4
|
+
return "constructor";
|
|
5
|
+
if (Node.isFunctionDeclaration(node) ||
|
|
6
|
+
Node.isClassDeclaration(node) ||
|
|
7
|
+
Node.isInterfaceDeclaration(node) ||
|
|
8
|
+
Node.isEnumDeclaration(node) ||
|
|
9
|
+
Node.isTypeAliasDeclaration(node) ||
|
|
10
|
+
Node.isModuleDeclaration(node) ||
|
|
11
|
+
Node.isMethodDeclaration(node) ||
|
|
12
|
+
Node.isMethodSignature(node) ||
|
|
13
|
+
Node.isPropertyDeclaration(node) ||
|
|
14
|
+
Node.isPropertySignature(node) ||
|
|
15
|
+
Node.isGetAccessorDeclaration(node) ||
|
|
16
|
+
Node.isSetAccessorDeclaration(node) ||
|
|
17
|
+
Node.isVariableDeclaration(node)) {
|
|
18
|
+
return node.getName();
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function addLocated(node, symbolPath, output) {
|
|
23
|
+
const name = namedNode(node);
|
|
24
|
+
if (!name)
|
|
25
|
+
return;
|
|
26
|
+
output.push({
|
|
27
|
+
node,
|
|
28
|
+
symbolPath,
|
|
29
|
+
name,
|
|
30
|
+
kind: node.getKindName(),
|
|
31
|
+
line: node.getStartLineNumber(),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function collectClassLike(node, prefix, output) {
|
|
35
|
+
if (!Node.isClassDeclaration(node) && !Node.isInterfaceDeclaration(node))
|
|
36
|
+
return;
|
|
37
|
+
for (const member of node.getMembers()) {
|
|
38
|
+
const name = namedNode(member);
|
|
39
|
+
if (name)
|
|
40
|
+
addLocated(member, `${prefix}.${name}`, output);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function moduleStatements(module) {
|
|
44
|
+
let body = module.getBody();
|
|
45
|
+
while (body && Node.isModuleDeclaration(body))
|
|
46
|
+
body = body.getBody();
|
|
47
|
+
return body && Node.isModuleBlock(body) ? body.getStatements() : [];
|
|
48
|
+
}
|
|
49
|
+
function collectStatements(statements, prefix, output) {
|
|
50
|
+
for (const statement of statements) {
|
|
51
|
+
if (Node.isVariableStatement(statement)) {
|
|
52
|
+
for (const declaration of statement.getDeclarations()) {
|
|
53
|
+
const path = prefix ? `${prefix}.${declaration.getName()}` : declaration.getName();
|
|
54
|
+
addLocated(declaration, path, output);
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const name = namedNode(statement);
|
|
59
|
+
if (!name)
|
|
60
|
+
continue;
|
|
61
|
+
const symbolPath = prefix ? `${prefix}.${name}` : name;
|
|
62
|
+
addLocated(statement, symbolPath, output);
|
|
63
|
+
collectClassLike(statement, symbolPath, output);
|
|
64
|
+
if (Node.isModuleDeclaration(statement)) {
|
|
65
|
+
collectStatements(moduleStatements(statement), symbolPath, output);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function collectSymbols(sourceFile) {
|
|
70
|
+
const symbols = [];
|
|
71
|
+
collectStatements(sourceFile.getStatements(), undefined, symbols);
|
|
72
|
+
return symbols;
|
|
73
|
+
}
|
|
74
|
+
function parseSelector(symbolPath) {
|
|
75
|
+
const match = /^(.*)@(\d+)$/.exec(symbolPath);
|
|
76
|
+
return match ? { path: match[1], line: Number(match[2]) } : { path: symbolPath };
|
|
77
|
+
}
|
|
78
|
+
export function findDeclaration(sourceFile, requestedPath) {
|
|
79
|
+
const selector = parseSelector(requestedPath);
|
|
80
|
+
let matches = collectSymbols(sourceFile).filter((symbol) => symbol.symbolPath === selector.path && (!selector.line || symbol.line === selector.line));
|
|
81
|
+
if (matches.length > 1) {
|
|
82
|
+
const implementations = matches.filter((symbol) => {
|
|
83
|
+
const node = symbol.node;
|
|
84
|
+
return ((Node.isFunctionDeclaration(node) || Node.isMethodDeclaration(node)) &&
|
|
85
|
+
node.getBody() !== undefined);
|
|
86
|
+
});
|
|
87
|
+
if (implementations.length === 1)
|
|
88
|
+
matches = implementations;
|
|
89
|
+
}
|
|
90
|
+
if (matches.length === 1)
|
|
91
|
+
return matches[0].node;
|
|
92
|
+
if (matches.length === 0) {
|
|
93
|
+
throw new Error(`Symbol "${requestedPath}" was not found in ${sourceFile.getFilePath()}. Use ast_get_outline or ast_search_symbols to obtain an exact symbol path.`);
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`Symbol "${requestedPath}" is ambiguous. Select one by line: ${matches
|
|
96
|
+
.map((symbol) => `${symbol.symbolPath}@${symbol.line}`)
|
|
97
|
+
.join(", ")}.`);
|
|
98
|
+
}
|
|
99
|
+
export function executableDeclaration(node) {
|
|
100
|
+
if (Node.isFunctionDeclaration(node) ||
|
|
101
|
+
Node.isMethodDeclaration(node) ||
|
|
102
|
+
Node.isConstructorDeclaration(node) ||
|
|
103
|
+
Node.isGetAccessorDeclaration(node) ||
|
|
104
|
+
Node.isSetAccessorDeclaration(node) ||
|
|
105
|
+
Node.isArrowFunction(node) ||
|
|
106
|
+
Node.isFunctionExpression(node)) {
|
|
107
|
+
return node;
|
|
108
|
+
}
|
|
109
|
+
if (Node.isVariableDeclaration(node) || Node.isPropertyDeclaration(node)) {
|
|
110
|
+
const initializer = node.getInitializer();
|
|
111
|
+
if (initializer &&
|
|
112
|
+
(Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))) {
|
|
113
|
+
return initializer;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`Symbol kind ${node.getKindName()} does not have a replaceable executable body.`);
|
|
117
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ts } from "ts-morph";
|
|
5
|
+
export function hashBytes(content) {
|
|
6
|
+
return createHash("sha256").update(content).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
export function hashWorkspaceFiles(files) {
|
|
9
|
+
const digest = createHash("sha256");
|
|
10
|
+
for (const [filePath, fileHash] of [...files].sort(([left], [right]) => left.localeCompare(right))) {
|
|
11
|
+
digest.update(filePath);
|
|
12
|
+
digest.update("\0");
|
|
13
|
+
digest.update(fileHash);
|
|
14
|
+
digest.update("\0");
|
|
15
|
+
}
|
|
16
|
+
return digest.digest("hex");
|
|
17
|
+
}
|
|
18
|
+
function canonicalPath(filePath) {
|
|
19
|
+
return fs.realpathSync(filePath);
|
|
20
|
+
}
|
|
21
|
+
function parseConfig(configPath) {
|
|
22
|
+
const source = ts.readJsonConfigFile(configPath, ts.sys.readFile);
|
|
23
|
+
const parsed = ts.parseJsonSourceFileConfigFileContent(source, ts.sys, path.dirname(configPath));
|
|
24
|
+
return {
|
|
25
|
+
extendedSourceFiles: source.extendedSourceFiles ?? [],
|
|
26
|
+
projectReferences: (parsed.projectReferences ?? []).map((reference) => reference.path),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function collectConfigPaths(rootConfigPath) {
|
|
30
|
+
const pending = [canonicalPath(rootConfigPath)];
|
|
31
|
+
const collected = new Set();
|
|
32
|
+
while (pending.length > 0) {
|
|
33
|
+
const configPath = pending.pop();
|
|
34
|
+
if (collected.has(configPath))
|
|
35
|
+
continue;
|
|
36
|
+
collected.add(configPath);
|
|
37
|
+
const { extendedSourceFiles, projectReferences } = parseConfig(configPath);
|
|
38
|
+
for (const extendedPath of extendedSourceFiles) {
|
|
39
|
+
if (fs.existsSync(extendedPath))
|
|
40
|
+
pending.push(canonicalPath(extendedPath));
|
|
41
|
+
}
|
|
42
|
+
for (const referencePath of projectReferences) {
|
|
43
|
+
if (!fs.existsSync(referencePath))
|
|
44
|
+
continue;
|
|
45
|
+
const candidate = fs.statSync(referencePath).isDirectory()
|
|
46
|
+
? path.join(referencePath, "tsconfig.json")
|
|
47
|
+
: referencePath;
|
|
48
|
+
if (fs.existsSync(candidate))
|
|
49
|
+
pending.push(canonicalPath(candidate));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return [...collected].sort();
|
|
53
|
+
}
|
|
54
|
+
export function createConfigSnapshot(rootConfigPath) {
|
|
55
|
+
const files = collectConfigPaths(rootConfigPath);
|
|
56
|
+
const digest = createHash("sha256");
|
|
57
|
+
for (const filePath of files) {
|
|
58
|
+
digest.update(filePath);
|
|
59
|
+
digest.update("\0");
|
|
60
|
+
digest.update(hashBytes(fs.readFileSync(filePath)));
|
|
61
|
+
digest.update("\0");
|
|
62
|
+
}
|
|
63
|
+
return { digest: digest.digest("hex"), files };
|
|
64
|
+
}
|
|
65
|
+
export function createWorkspaceSnapshot(context) {
|
|
66
|
+
const sourcePaths = context.project
|
|
67
|
+
.getSourceFiles()
|
|
68
|
+
.map((sourceFile) => sourceFile.getFilePath());
|
|
69
|
+
const paths = [...sourcePaths, ...createConfigSnapshot(context.tsConfigFilePath).files]
|
|
70
|
+
.map(canonicalPath)
|
|
71
|
+
.sort();
|
|
72
|
+
const files = new Map();
|
|
73
|
+
for (const filePath of paths) {
|
|
74
|
+
const digest = hashBytes(fs.readFileSync(filePath));
|
|
75
|
+
const previous = files.get(filePath);
|
|
76
|
+
if (previous && previous !== digest) {
|
|
77
|
+
throw new Error(`Workspace identity collision for ${filePath}.`);
|
|
78
|
+
}
|
|
79
|
+
files.set(filePath, digest);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
digest: hashWorkspaceFiles(files),
|
|
83
|
+
fileCount: files.size,
|
|
84
|
+
files,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { applyOperation } from "../services/operations.js";
|
|
3
|
+
import { errorResult, structuredResult } from "./result.js";
|
|
4
|
+
const AstApplyOperationInputSchema = z.object({
|
|
5
|
+
operation_id: z.string().uuid().describe("Identifier returned by a prepare operation."),
|
|
6
|
+
plan_hash: z.string().length(64).describe("Plan hash returned by the same prepare operation."),
|
|
7
|
+
});
|
|
8
|
+
const AstApplyOperationOutputSchema = z.object({
|
|
9
|
+
operation_id: z.string().uuid(),
|
|
10
|
+
kind: z.enum(["rename_symbol", "replace_symbol_body"]),
|
|
11
|
+
status: z.literal("applied"),
|
|
12
|
+
applied_at: z.string(),
|
|
13
|
+
affected_files: z.array(z.string()),
|
|
14
|
+
idempotent_replay: z.boolean(),
|
|
15
|
+
});
|
|
16
|
+
export function registerApplyOperation(server) {
|
|
17
|
+
server.registerTool("ast_apply_operation", {
|
|
18
|
+
title: "Apply a prepared structural operation",
|
|
19
|
+
description: "Applies exactly the reviewed plan after verifying its plan hash and the complete TypeScript workspace fingerprint. Conflicts abort before writes; retries after success are idempotent.",
|
|
20
|
+
inputSchema: AstApplyOperationInputSchema,
|
|
21
|
+
outputSchema: AstApplyOperationOutputSchema,
|
|
22
|
+
annotations: {
|
|
23
|
+
readOnlyHint: false,
|
|
24
|
+
destructiveHint: true,
|
|
25
|
+
idempotentHint: true,
|
|
26
|
+
openWorldHint: false,
|
|
27
|
+
},
|
|
28
|
+
}, async ({ operation_id, plan_hash }) => {
|
|
29
|
+
try {
|
|
30
|
+
return structuredResult({ ...(await applyOperation(operation_id, plan_hash)) });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
return errorResult(error);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Node } from "ts-morph";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { PaginationInputSchema, PaginationOutputSchema, paginate } from "../services/pagination.js";
|
|
5
|
+
import { findDeclarationByName, getSourceFileOrThrow, withProject } from "../services/project.js";
|
|
6
|
+
import { errorResult, structuredResult } from "./result.js";
|
|
7
|
+
const AstFindReferencesInputSchema = z.object({
|
|
8
|
+
project_root: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe("Absolute project directory containing tsconfig.json, or the config path."),
|
|
11
|
+
file_path: z.string().describe("File containing the declaration."),
|
|
12
|
+
symbol_path: z.string().describe("Exact symbol path returned by an outline or symbol search."),
|
|
13
|
+
include_declaration: z.boolean().default(true),
|
|
14
|
+
...PaginationInputSchema,
|
|
15
|
+
});
|
|
16
|
+
const ReferenceSchema = z.object({
|
|
17
|
+
file: z.string(),
|
|
18
|
+
line: z.number().int().positive(),
|
|
19
|
+
column: z.number().int().positive(),
|
|
20
|
+
kind: z.string(),
|
|
21
|
+
is_declaration: z.boolean(),
|
|
22
|
+
context: z.string(),
|
|
23
|
+
});
|
|
24
|
+
const AstFindReferencesOutputSchema = z.object({
|
|
25
|
+
symbol: z.string(),
|
|
26
|
+
declaration_count: z.number().int().min(0),
|
|
27
|
+
reference_count: z.number().int().min(0),
|
|
28
|
+
affected_files: z.array(z.string()),
|
|
29
|
+
references: z.array(ReferenceSchema),
|
|
30
|
+
...PaginationOutputSchema,
|
|
31
|
+
});
|
|
32
|
+
export function registerFindReferences(server) {
|
|
33
|
+
server.registerTool("ast_find_references", {
|
|
34
|
+
title: "Find semantic symbol references",
|
|
35
|
+
description: "Finds type-resolved references and returns bounded project-relative locations, including declaration impact by default.",
|
|
36
|
+
inputSchema: AstFindReferencesInputSchema,
|
|
37
|
+
outputSchema: AstFindReferencesOutputSchema,
|
|
38
|
+
annotations: {
|
|
39
|
+
readOnlyHint: true,
|
|
40
|
+
destructiveHint: false,
|
|
41
|
+
idempotentHint: true,
|
|
42
|
+
openWorldHint: false,
|
|
43
|
+
},
|
|
44
|
+
}, async ({ project_root, file_path, symbol_path, include_declaration, offset, limit }) => {
|
|
45
|
+
try {
|
|
46
|
+
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
47
|
+
const sourceFile = getSourceFileOrThrow(project, file_path);
|
|
48
|
+
const node = findDeclarationByName(sourceFile, symbol_path);
|
|
49
|
+
if (!Node.isReferenceFindable(node)) {
|
|
50
|
+
throw new Error(`Symbol "${symbol_path}" (${node.getKindName()}) does not support semantic reference search.`);
|
|
51
|
+
}
|
|
52
|
+
const lineCache = new Map();
|
|
53
|
+
const location = (referenceNode, isDeclaration) => {
|
|
54
|
+
const referenceSource = referenceNode.getSourceFile();
|
|
55
|
+
const absoluteFile = referenceSource.getFilePath();
|
|
56
|
+
let lines = lineCache.get(absoluteFile);
|
|
57
|
+
if (!lines) {
|
|
58
|
+
lines = referenceSource.getFullText().split(/\r?\n/);
|
|
59
|
+
lineCache.set(absoluteFile, lines);
|
|
60
|
+
}
|
|
61
|
+
const position = referenceSource.getLineAndColumnAtPos(referenceNode.getStart());
|
|
62
|
+
return {
|
|
63
|
+
file: path.relative(projectRoot, absoluteFile),
|
|
64
|
+
line: position.line,
|
|
65
|
+
column: position.column,
|
|
66
|
+
kind: referenceNode.getParent()?.getKindName() ?? referenceNode.getKindName(),
|
|
67
|
+
is_declaration: isDeclaration,
|
|
68
|
+
context: (lines[position.line - 1] ?? "").trim().slice(0, 500),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
const references = node
|
|
72
|
+
.findReferencesAsNodes()
|
|
73
|
+
.map((reference) => location(reference, false));
|
|
74
|
+
const declaration = location(node, true);
|
|
75
|
+
const allLocations = include_declaration ? [declaration, ...references] : references;
|
|
76
|
+
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}`));
|
|
77
|
+
const affectedFiles = [
|
|
78
|
+
...new Set([declaration, ...references].map((item) => item.file)),
|
|
79
|
+
].sort();
|
|
80
|
+
const page = paginate(allLocations, offset, limit);
|
|
81
|
+
const { items, ...metadata } = page;
|
|
82
|
+
return {
|
|
83
|
+
symbol: symbol_path,
|
|
84
|
+
declaration_count: 1,
|
|
85
|
+
reference_count: references.length,
|
|
86
|
+
affected_files: affectedFiles,
|
|
87
|
+
references: items,
|
|
88
|
+
...metadata,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
return structuredResult(structuredContent);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return errorResult(error);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { normalizeDiagnostic } from "../services/diagnostics.js";
|
|
3
|
+
import { PaginationInputSchema, PaginationOutputSchema, paginate } from "../services/pagination.js";
|
|
4
|
+
import { getSourceFileOrThrow, withProject } from "../services/project.js";
|
|
5
|
+
import { errorResult, structuredResult } from "./result.js";
|
|
6
|
+
const DiagnosticSchema = z.object({
|
|
7
|
+
code: z.number().int(),
|
|
8
|
+
category: z.string(),
|
|
9
|
+
file: z.string().nullable(),
|
|
10
|
+
line: z.number().int().positive().nullable(),
|
|
11
|
+
column: z.number().int().positive().nullable(),
|
|
12
|
+
message: z.string(),
|
|
13
|
+
});
|
|
14
|
+
const AstGetDiagnosticsInputSchema = z.object({
|
|
15
|
+
project_root: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe("Absolute project directory containing tsconfig.json, or the config path."),
|
|
18
|
+
file_path: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Optional project-relative or absolute file path. Omit for project diagnostics."),
|
|
22
|
+
...PaginationInputSchema,
|
|
23
|
+
});
|
|
24
|
+
const AstGetDiagnosticsOutputSchema = z.object({
|
|
25
|
+
diagnostics: z.array(DiagnosticSchema),
|
|
26
|
+
error_count: z.number().int().min(0),
|
|
27
|
+
warning_count: z.number().int().min(0),
|
|
28
|
+
duration_ms: z.number().min(0),
|
|
29
|
+
...PaginationOutputSchema,
|
|
30
|
+
});
|
|
31
|
+
export function registerGetDiagnostics(server) {
|
|
32
|
+
server.registerTool("ast_get_diagnostics", {
|
|
33
|
+
title: "Get TypeScript diagnostics",
|
|
34
|
+
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
|
+
inputSchema: AstGetDiagnosticsInputSchema,
|
|
36
|
+
outputSchema: AstGetDiagnosticsOutputSchema,
|
|
37
|
+
annotations: {
|
|
38
|
+
readOnlyHint: true,
|
|
39
|
+
destructiveHint: false,
|
|
40
|
+
idempotentHint: true,
|
|
41
|
+
openWorldHint: false,
|
|
42
|
+
},
|
|
43
|
+
}, async ({ project_root, file_path, offset, limit }) => {
|
|
44
|
+
try {
|
|
45
|
+
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
46
|
+
const startedAt = performance.now();
|
|
47
|
+
const diagnostics = file_path
|
|
48
|
+
? getSourceFileOrThrow(project, file_path).getPreEmitDiagnostics()
|
|
49
|
+
: [...project.getConfigFileParsingDiagnostics(), ...project.getPreEmitDiagnostics()];
|
|
50
|
+
const normalized = diagnostics
|
|
51
|
+
.map((diagnostic) => normalizeDiagnostic(diagnostic, projectRoot))
|
|
52
|
+
.sort((left, right) => `${left.file ?? ""}:${left.line ?? 0}:${left.column ?? 0}:${left.code}`.localeCompare(`${right.file ?? ""}:${right.line ?? 0}:${right.column ?? 0}:${right.code}`));
|
|
53
|
+
const page = paginate(normalized, offset, limit);
|
|
54
|
+
const { items, ...metadata } = page;
|
|
55
|
+
return {
|
|
56
|
+
diagnostics: items,
|
|
57
|
+
error_count: normalized.filter((diagnostic) => diagnostic.category === "Error").length,
|
|
58
|
+
warning_count: normalized.filter((diagnostic) => diagnostic.category === "Warning")
|
|
59
|
+
.length,
|
|
60
|
+
duration_ms: performance.now() - startedAt,
|
|
61
|
+
...metadata,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
return structuredResult(structuredContent);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return errorResult(error);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getOperationPreview } from "../services/operations.js";
|
|
3
|
+
import { errorResult, structuredResult } from "./result.js";
|
|
4
|
+
const AstGetOperationPreviewInputSchema = z.object({
|
|
5
|
+
operation_id: z.string().uuid(),
|
|
6
|
+
file: z.string().optional().describe("Optional affected file path to retrieve only its diff."),
|
|
7
|
+
});
|
|
8
|
+
const AstGetOperationPreviewOutputSchema = z.object({
|
|
9
|
+
operation_id: z.string().uuid(),
|
|
10
|
+
plan_hash: z.string().length(64),
|
|
11
|
+
files: z.array(z.object({ file: z.string(), diff: z.string() })),
|
|
12
|
+
});
|
|
13
|
+
export function registerGetOperationPreview(server) {
|
|
14
|
+
server.registerTool("ast_get_operation_preview", {
|
|
15
|
+
title: "Get complete prepared-operation diff",
|
|
16
|
+
description: "Retrieves exact unified diffs retained for a prepared operation, optionally one affected file at a time.",
|
|
17
|
+
inputSchema: AstGetOperationPreviewInputSchema,
|
|
18
|
+
outputSchema: AstGetOperationPreviewOutputSchema,
|
|
19
|
+
annotations: {
|
|
20
|
+
readOnlyHint: true,
|
|
21
|
+
destructiveHint: false,
|
|
22
|
+
idempotentHint: true,
|
|
23
|
+
openWorldHint: false,
|
|
24
|
+
},
|
|
25
|
+
}, async ({ operation_id, file }) => {
|
|
26
|
+
try {
|
|
27
|
+
return structuredResult(getOperationPreview(operation_id, file));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return errorResult(error);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { buildFileOutline } from "../services/outline.js";
|
|
4
|
+
import { getSourceFileOrThrow, withProject } from "../services/project.js";
|
|
5
|
+
import { errorResult, structuredResult } from "./result.js";
|
|
6
|
+
const AstGetOutlineInputSchema = z.object({
|
|
7
|
+
project_root: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe("Absolute project directory containing tsconfig.json, or the config path."),
|
|
10
|
+
file_path: z.string().describe("Project-relative or absolute source file path."),
|
|
11
|
+
include_symbols: z
|
|
12
|
+
.boolean()
|
|
13
|
+
.default(false)
|
|
14
|
+
.describe("Include detailed symbol metadata. Omit for the smallest body-free response."),
|
|
15
|
+
});
|
|
16
|
+
const OutlineSymbolSchema = z.object({
|
|
17
|
+
symbolPath: z.string(),
|
|
18
|
+
name: z.string(),
|
|
19
|
+
kind: z.string(),
|
|
20
|
+
signature: z.string(),
|
|
21
|
+
startLine: z.number().int().positive(),
|
|
22
|
+
endLine: z.number().int().positive(),
|
|
23
|
+
});
|
|
24
|
+
const AstGetOutlineOutputSchema = z.object({
|
|
25
|
+
file: z.string(),
|
|
26
|
+
outline: z.string(),
|
|
27
|
+
symbols: z.array(OutlineSymbolSchema).optional(),
|
|
28
|
+
});
|
|
29
|
+
export function registerGetOutline(server) {
|
|
30
|
+
server.registerTool("ast_get_outline", {
|
|
31
|
+
title: "Get compact file outline",
|
|
32
|
+
description: "Returns body-free declaration signatures for one TypeScript/JavaScript file. Detailed symbol metadata is opt-in because it duplicates signature text.",
|
|
33
|
+
inputSchema: AstGetOutlineInputSchema,
|
|
34
|
+
outputSchema: AstGetOutlineOutputSchema,
|
|
35
|
+
annotations: {
|
|
36
|
+
readOnlyHint: true,
|
|
37
|
+
destructiveHint: false,
|
|
38
|
+
idempotentHint: true,
|
|
39
|
+
openWorldHint: false,
|
|
40
|
+
},
|
|
41
|
+
}, async ({ project_root, file_path, include_symbols }) => {
|
|
42
|
+
try {
|
|
43
|
+
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
44
|
+
const sourceFile = getSourceFileOrThrow(project, file_path);
|
|
45
|
+
const outline = buildFileOutline(sourceFile);
|
|
46
|
+
return {
|
|
47
|
+
file: path.relative(projectRoot, sourceFile.getFilePath()),
|
|
48
|
+
outline: outline.text,
|
|
49
|
+
...(include_symbols ? { symbols: outline.symbols } : {}),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
return structuredResult(structuredContent);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return errorResult(error);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { nodeSourceWithLocation } from "../services/outline.js";
|
|
3
|
+
import { findDeclarationByName, getSourceFileOrThrow, withProject } from "../services/project.js";
|
|
4
|
+
import { errorResult, structuredResult } from "./result.js";
|
|
5
|
+
const AstGetSymbolSourceInputSchema = z.object({
|
|
6
|
+
project_root: z
|
|
7
|
+
.string()
|
|
8
|
+
.describe("Absolute project directory containing tsconfig.json, or the config path."),
|
|
9
|
+
file_path: z.string().describe("Project-relative or absolute source file path."),
|
|
10
|
+
symbol_path: z.string().describe('Symbol path such as "formatDate" or "UserService.create".'),
|
|
11
|
+
});
|
|
12
|
+
const AstGetSymbolSourceOutputSchema = z.object({
|
|
13
|
+
file: z.string(),
|
|
14
|
+
startLine: z.number().int().positive(),
|
|
15
|
+
endLine: z.number().int().positive(),
|
|
16
|
+
text: z.string(),
|
|
17
|
+
});
|
|
18
|
+
export function registerGetSymbolSource(server) {
|
|
19
|
+
server.registerTool("ast_get_symbol_source", {
|
|
20
|
+
title: "Get one symbol implementation",
|
|
21
|
+
description: "Returns exactly one declaration or implementation with its project-relative location, not the complete file.",
|
|
22
|
+
inputSchema: AstGetSymbolSourceInputSchema,
|
|
23
|
+
outputSchema: AstGetSymbolSourceOutputSchema,
|
|
24
|
+
annotations: {
|
|
25
|
+
readOnlyHint: true,
|
|
26
|
+
destructiveHint: false,
|
|
27
|
+
idempotentHint: true,
|
|
28
|
+
openWorldHint: false,
|
|
29
|
+
},
|
|
30
|
+
}, async ({ project_root, file_path, symbol_path }) => {
|
|
31
|
+
try {
|
|
32
|
+
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
33
|
+
const sourceFile = getSourceFileOrThrow(project, file_path);
|
|
34
|
+
const node = findDeclarationByName(sourceFile, symbol_path);
|
|
35
|
+
return nodeSourceWithLocation(node, projectRoot);
|
|
36
|
+
});
|
|
37
|
+
return structuredResult(structuredContent);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return errorResult(error);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|