jsc-typescript-ast-mcp 1.0.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/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { registerComponentTreeTool } from './ts-morph/componentTree.js';
4
+ import { registerFindDependencyTool } from './ts-morph/dependency.js';
5
+ import { registerFindReferenceTool } from './ts-morph/references.js';
6
+ // Create server instance
7
+ const jscServer = new McpServer({
8
+ name: 'typescript-ast',
9
+ version: '1.0.0',
10
+ });
11
+ const server = new Proxy(jscServer, {
12
+ get(target, prop) {
13
+ if (prop === 'registerTool') {
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ return (name, options, handler) => {
16
+ console.error(`Registering tool: ${name}`);
17
+ return target.registerTool(name, options, handler);
18
+ };
19
+ }
20
+ return Reflect.get(target, prop);
21
+ },
22
+ });
23
+ registerFindReferenceTool(server);
24
+ registerFindDependencyTool(server);
25
+ registerComponentTreeTool(server);
26
+ const main = async () => {
27
+ const transport = new StdioServerTransport();
28
+ await server.connect(transport);
29
+ console.error('TypeScript AST MCP Server running on stdio');
30
+ };
31
+ main().catch((error) => {
32
+ console.error('Error starting TypeScript AST MCP Server:', error);
33
+ process.exit(1);
34
+ });
@@ -0,0 +1,37 @@
1
+ import z from 'zod';
2
+ import { createProject } from './project.js';
3
+ import { analyzeComponent } from './utils/analyzeComponent.js';
4
+ import { getComponent } from './utils/component.js';
5
+ export const registerComponentTreeTool = (server) => {
6
+ server.registerTool('component_tree', {
7
+ description: 'Get the component tree for a given entry file and maximum depth',
8
+ inputSchema: {
9
+ entryFilePath: z.string().describe('Path to the entry file'),
10
+ maxDepth: z
11
+ .number()
12
+ .default(3)
13
+ .describe('Maximum depth of the component tree (default: 3)'),
14
+ },
15
+ }, async ({ entryFilePath, maxDepth }) => {
16
+ const tree = getComponentTree(entryFilePath, maxDepth);
17
+ return {
18
+ content: [
19
+ {
20
+ type: 'text',
21
+ text: JSON.stringify(tree, null, 2),
22
+ },
23
+ ],
24
+ };
25
+ });
26
+ };
27
+ const getComponentTree = (entryFilePath, maxDepth) => {
28
+ const project = createProject();
29
+ const sourceFile = project.getSourceFile(entryFilePath);
30
+ if (!sourceFile) {
31
+ throw new Error(`File not found: ${entryFilePath}`);
32
+ }
33
+ const component = getComponent(sourceFile);
34
+ return analyzeComponent(component, project, {
35
+ maxDepth,
36
+ }, 0);
37
+ };
@@ -0,0 +1,42 @@
1
+ import z from 'zod';
2
+ import { createProject } from './project.js';
3
+ export const registerFindDependencyTool = (server) => {
4
+ server.registerTool('find_dependency', {
5
+ description: 'Find files with a given dependency',
6
+ inputSchema: {
7
+ dependencyName: z
8
+ .string()
9
+ .describe('Name of the dependency to search for'),
10
+ },
11
+ }, async ({ dependencyName }) => {
12
+ const project = createProject();
13
+ const files = getFilesWithDependency(project, dependencyName);
14
+ return {
15
+ content: [
16
+ {
17
+ type: 'text',
18
+ text: files.join('\n'),
19
+ },
20
+ ],
21
+ };
22
+ });
23
+ };
24
+ const getFilesWithDependency = (project, dependencyName) => {
25
+ const sourceFiles = project.getSourceFiles();
26
+ const filesWithDependency = [];
27
+ sourceFiles.forEach((sourceFile) => {
28
+ const importDeclarations = sourceFile.getImportDeclarations();
29
+ importDeclarations.forEach((importDeclaration) => {
30
+ const moduleSpecifier = importDeclaration.getModuleSpecifierValue();
31
+ if (moduleSpecifier === dependencyName ||
32
+ moduleSpecifier.includes(`${dependencyName}/`)) {
33
+ filesWithDependency.push(sourceFile.getFilePath());
34
+ }
35
+ });
36
+ });
37
+ return filesWithDependency;
38
+ };
39
+ export const findFilesWithDependency = (dependencyName) => {
40
+ const project = createProject();
41
+ return getFilesWithDependency(project, dependencyName);
42
+ };
@@ -0,0 +1,51 @@
1
+ import * as dotenv from 'dotenv'
2
+ import { Project } from 'ts-morph'
3
+ import { z } from 'zod'
4
+ dotenv.config()
5
+ export const registerTsMorphTools = (server) => {
6
+ server.registerTool(
7
+ 'find_reference',
8
+ {
9
+ description: 'Find references for a given class and method',
10
+ inputSchema: {
11
+ filePath: z.string().describe('Path to the file'),
12
+ className: z.string().describe('Name of the class'),
13
+ methodName: z.string().describe('Name of the method'),
14
+ },
15
+ },
16
+ async ({ filePath, className, methodName }) => {
17
+ const project = createProject()
18
+ const references = getReferences(project, filePath, className, methodName)
19
+ return {
20
+ content: [
21
+ {
22
+ type: 'text',
23
+ text: references.join('\n'),
24
+ },
25
+ ],
26
+ }
27
+ },
28
+ )
29
+ }
30
+ const createProject = () => {
31
+ const tsConfigFilePath = process.env.PROJECT_TSCONFIG_PATH || 'tsconfig.json'
32
+ console.error(`Using tsconfig file at: ${tsConfigFilePath}`)
33
+ return new Project({
34
+ tsConfigFilePath,
35
+ })
36
+ }
37
+ const getReferences = (project, fileName, className, methodName) => {
38
+ const source = project.getSourceFileOrThrow(fileName)
39
+ const method = source.getClass(className).getMethod(methodName)
40
+ const references = method.findReferences()
41
+ const referencePaths = []
42
+ references.forEach((reference) => {
43
+ reference.getReferences().forEach((currentReference) => {
44
+ const referenceNode = currentReference.getNode()
45
+ const lineNumber = referenceNode.getStartLineNumber()
46
+ const filePath = referenceNode.getSourceFile().getFilePath()
47
+ referencePaths.push(`${filePath}:${lineNumber}`)
48
+ })
49
+ })
50
+ return referencePaths
51
+ }
@@ -0,0 +1,10 @@
1
+ import * as dotenv from 'dotenv';
2
+ import { Project } from 'ts-morph';
3
+ dotenv.config({ quiet: true });
4
+ export const createProject = () => {
5
+ const tsConfigFilePath = process.env.PROJECT_TSCONFIG_PATH || 'tsconfig.json';
6
+ console.error(`Using tsconfig file at: ${tsConfigFilePath}`);
7
+ return new Project({
8
+ tsConfigFilePath,
9
+ });
10
+ };
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ import { createProject } from './project.js';
3
+ export const registerFindReferenceTool = (server) => {
4
+ server.registerTool('find_reference', {
5
+ description: 'Find references for a given class and method',
6
+ inputSchema: {
7
+ filePath: z.string().describe('Path to the file'),
8
+ className: z.string().describe('Name of the class'),
9
+ methodName: z.string().describe('Name of the method'),
10
+ },
11
+ }, async ({ filePath, className, methodName }) => {
12
+ const project = createProject();
13
+ const references = getReferences(project, filePath, className, methodName);
14
+ return {
15
+ content: [
16
+ {
17
+ type: 'text',
18
+ text: references.join('\n'),
19
+ },
20
+ ],
21
+ };
22
+ });
23
+ };
24
+ const getReferences = (project, fileName, className, methodName) => {
25
+ const source = project.getSourceFileOrThrow(fileName);
26
+ const method = source.getClass(className).getMethod(methodName);
27
+ const references = method.findReferences();
28
+ const referencePaths = [];
29
+ references.forEach((reference) => {
30
+ reference.getReferences().forEach((currentReference) => {
31
+ const referenceNode = currentReference.getNode();
32
+ const lineNumber = referenceNode.getStartLineNumber();
33
+ const filePath = referenceNode.getSourceFile().getFilePath();
34
+ referencePaths.push(`${filePath}:${lineNumber}`);
35
+ });
36
+ });
37
+ return referencePaths;
38
+ };
@@ -0,0 +1,19 @@
1
+ import { extractJSX, findReturnedJSX } from './extractJsx.js';
2
+ import { getComponentName } from './nameHelper.js';
3
+ export const analyzeComponent = (node, project, options, currentDepth) => {
4
+ const name = getComponentName(node);
5
+ console.error(`Analyzing component: ${name}, depth: ${currentDepth}`);
6
+ const root = {
7
+ name,
8
+ type: 'component',
9
+ children: [],
10
+ };
11
+ if (currentDepth >= options.maxDepth) {
12
+ return root;
13
+ }
14
+ const jsx = findReturnedJSX(node);
15
+ if (!jsx)
16
+ return root;
17
+ root.children = extractJSX(jsx, project, options, currentDepth + 1);
18
+ return root;
19
+ };
@@ -0,0 +1,16 @@
1
+ import { SyntaxKind } from 'ts-morph';
2
+ import { getComponentName } from './nameHelper.js';
3
+ export const getComponent = (sourceFile) => {
4
+ const component = sourceFile.getDefaultExportSymbol()?.getDeclarations()?.[0];
5
+ if (!component) {
6
+ throw new Error(`Component not found in ${sourceFile.getFilePath()}`);
7
+ }
8
+ const componentName = getComponentName(component);
9
+ if (componentName !== 'AnonymousComponent')
10
+ return component;
11
+ const components = sourceFile.getVariableDeclarations().filter((v) => {
12
+ const initializer = v.getInitializer();
13
+ return initializer?.getKind() === SyntaxKind.ArrowFunction;
14
+ });
15
+ return components[0];
16
+ };
@@ -0,0 +1,164 @@
1
+ import { Node, SyntaxKind, } from 'ts-morph';
2
+ import { analyzeComponent } from './analyzeComponent.js';
3
+ import { getComponent } from './component.js';
4
+ import { getTagName } from './nameHelper.js';
5
+ import { handleTernary } from './ternary.js';
6
+ const extractFromExpression = (expression, project, options, depth) => {
7
+ if (Node.isConditionalExpression(expression)) {
8
+ return handleTernary(expression, project, options, depth);
9
+ }
10
+ if (Node.isBinaryExpression(expression)) {
11
+ return handleLogicalAnd(expression, project, options, depth);
12
+ }
13
+ if (Node.isIdentifier(expression)) {
14
+ return extractFromIdentifier(expression, project, options, depth);
15
+ }
16
+ return extractJSX(expression, project, options, depth);
17
+ };
18
+ const handleLogicalAnd = (expression, project, options, depth) => {
19
+ if (expression.getOperatorToken().getText() !== '&&') {
20
+ return [];
21
+ }
22
+ const condition = expression.getLeft().getText();
23
+ const right = expression.getRight();
24
+ const nodes = extractJSX(right, project, options, depth);
25
+ nodes.forEach((n) => {
26
+ n.condition = { expression: condition };
27
+ });
28
+ return nodes;
29
+ };
30
+ const extractFromIdentifier = (expression, project, options, depth) => {
31
+ const declaration = expression.getSymbol()?.getDeclarations()?.[0];
32
+ if (!declaration || !Node.isVariableDeclaration(declaration)) {
33
+ return [];
34
+ }
35
+ const initializer = declaration.getInitializer();
36
+ if (!initializer) {
37
+ return [];
38
+ }
39
+ return extractFromExpression(initializer, project, options, depth);
40
+ };
41
+ const buildNodeFromJsxFragment = (node, project, options, depth) => ({
42
+ name: 'Fragment',
43
+ type: 'fragment',
44
+ children: extractJSX(node, project, options, depth),
45
+ });
46
+ export const extractJSX = (node, project, options, depth) => {
47
+ const results = [];
48
+ node.forEachChild((child) => {
49
+ // 1. JSX Elements
50
+ if (Node.isJsxElement(child) || Node.isJsxSelfClosingElement(child)) {
51
+ const element = buildNodeFromJSX(child, project, options, depth);
52
+ if (element)
53
+ results.push(element);
54
+ return;
55
+ }
56
+ // 2. Fragment
57
+ if (Node.isJsxFragment(child)) {
58
+ results.push(buildNodeFromJsxFragment(child, project, options, depth));
59
+ return;
60
+ }
61
+ // 3. JSX Expression container
62
+ if (Node.isJsxExpression(child)) {
63
+ const expression = child.getExpression();
64
+ if (!expression) {
65
+ return;
66
+ }
67
+ results.push(...extractFromExpression(expression, project, options, depth));
68
+ return;
69
+ }
70
+ // 4. Ternary
71
+ if (Node.isConditionalExpression(child)) {
72
+ results.push(...handleTernary(child, project, options, depth));
73
+ return;
74
+ }
75
+ // 5. Logical AND
76
+ if (Node.isBinaryExpression(child)) {
77
+ results.push(...handleLogicalAnd(child, project, options, depth));
78
+ return;
79
+ }
80
+ results.push(...extractJSX(child, project, options, depth));
81
+ });
82
+ return results;
83
+ };
84
+ const resolveComponentFile = (node) => {
85
+ const tagName = getTagName(node);
86
+ const sourceFile = node.getSourceFile();
87
+ const importDecls = sourceFile.getImportDeclarations();
88
+ for (const imp of importDecls) {
89
+ const named = imp.getNamedImports();
90
+ const defaultImport = imp.getDefaultImport();
91
+ if (defaultImport?.getText() === tagName) {
92
+ const file = imp.getModuleSpecifierSourceFile();
93
+ const component = getComponent(file);
94
+ return component ?? null;
95
+ }
96
+ for (const n of named) {
97
+ if (n.getName() === tagName) {
98
+ const file = imp.getModuleSpecifierSourceFile();
99
+ const component = getComponent(file);
100
+ return component ?? null;
101
+ }
102
+ }
103
+ }
104
+ return null;
105
+ };
106
+ function buildNodeFromJSX(node, project, options, depth) {
107
+ const tagName = getTagName(node);
108
+ const isHtml = tagName[0] === tagName[0].toLowerCase();
109
+ const treeNode = {
110
+ name: tagName,
111
+ type: isHtml ? 'html' : 'component',
112
+ children: [],
113
+ };
114
+ // Recurse into children
115
+ if (Node.isJsxElement(node)) {
116
+ const children = node.getJsxChildren();
117
+ children.forEach((child) => {
118
+ if (Node.isJsxElement(child) || Node.isJsxSelfClosingElement(child)) {
119
+ const childNode = buildNodeFromJSX(child, project, options, depth);
120
+ if (childNode) {
121
+ treeNode.children.push(childNode);
122
+ }
123
+ return;
124
+ }
125
+ if (Node.isJsxFragment(child)) {
126
+ treeNode.children.push(buildNodeFromJsxFragment(child, project, options, depth));
127
+ return;
128
+ }
129
+ if (Node.isJsxExpression(child)) {
130
+ const expression = child.getExpression();
131
+ if (expression) {
132
+ treeNode.children.push(...extractFromExpression(expression, project, options, depth));
133
+ }
134
+ return;
135
+ }
136
+ treeNode.children.push(...extractJSX(child, project, options, depth));
137
+ });
138
+ }
139
+ // Resolve component if custom
140
+ if (!isHtml && depth < options.maxDepth) {
141
+ const resolved = resolveComponentFile(node);
142
+ if (resolved) {
143
+ const subTree = analyzeComponent(resolved, project, options, depth);
144
+ treeNode.children.push(...subTree.children);
145
+ }
146
+ }
147
+ return treeNode;
148
+ }
149
+ export const findReturnedJSX = (node) => {
150
+ const returnStmt = node.getDescendantsOfKind(SyntaxKind.ReturnStatement)[0];
151
+ if (returnStmt) {
152
+ return returnStmt.getExpression() ?? null;
153
+ }
154
+ // Arrow function implicit return
155
+ if (Node.isArrowFunction(node)) {
156
+ const body = node.getBody();
157
+ if (Node.isJsxElement(body) ||
158
+ Node.isJsxSelfClosingElement(body) ||
159
+ Node.isJsxFragment(body)) {
160
+ return body;
161
+ }
162
+ }
163
+ return null;
164
+ };
@@ -0,0 +1,16 @@
1
+ import { Node } from 'ts-morph';
2
+ export const getTagName = (node) => {
3
+ if (Node.isJsxElement(node)) {
4
+ return node.getOpeningElement().getTagNameNode().getText();
5
+ }
6
+ return node.getTagNameNode().getText();
7
+ };
8
+ export const getComponentName = (node) => {
9
+ if (Node.isFunctionDeclaration(node) && node.getName()) {
10
+ return node.getName();
11
+ }
12
+ if (Node.isVariableDeclaration(node)) {
13
+ return node.getName();
14
+ }
15
+ return 'AnonymousComponent';
16
+ };
@@ -0,0 +1,13 @@
1
+ import { extractJSX } from './extractJsx.js';
2
+ export const handleTernary = (node, project, options, depth) => {
3
+ const condition = node.getCondition().getText();
4
+ const whenTrue = extractJSX(node.getWhenTrue(), project, options, depth);
5
+ const whenFalse = extractJSX(node.getWhenFalse(), project, options, depth);
6
+ whenTrue.forEach((n) => {
7
+ n.condition = { expression: condition };
8
+ });
9
+ whenFalse.forEach((n) => {
10
+ n.condition = { expression: `!(${condition})` };
11
+ });
12
+ return [...whenTrue, ...whenFalse];
13
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "jsc-typescript-ast-mcp",
3
+ "version": "1.0.0",
4
+ "description": "A Model Context Protocol (MCP) server that provides an abstract syntax tree (AST) representation of TypeScript code using the ts-morph library.",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "format": "prettier --write .",
8
+ "build": "tsc",
9
+ "test:mcp": "npx @modelcontextprotocol/inspector node dist/index.js"
10
+ },
11
+ "bin": {
12
+ "typescript-ast": "./dist/index.js"
13
+ },
14
+ "type": "module",
15
+ "license": "MIT",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "devDependencies": {
20
+ "@eslint/js": "^10.0.1",
21
+ "@types/node": "^25.3.5",
22
+ "eslint": "^10.0.2",
23
+ "prettier": "^3.8.1",
24
+ "prettier-plugin-organize-imports": "^4.3.0",
25
+ "typescript": "^5.9.3",
26
+ "typescript-eslint": "^8.56.1"
27
+ },
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.27.1",
30
+ "dotenv": "^17.3.1",
31
+ "ts-morph": "^27.0.2",
32
+ "zod": "^4.3.6"
33
+ }
34
+ }