jsc-typescript-ast-mcp 1.0.11 → 1.1.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/README.md CHANGED
@@ -96,6 +96,7 @@ Analyze React component structures in your codebase.
96
96
  - **Input**:
97
97
  - `entryFilePath`: Path to the entry file
98
98
  - `maxDepth`: Maximum depth of the component tree (default: 3)
99
+ - `data-id` (optional): Attribute name to capture into `TreeNode.dataId` (example: `data-attribute-id`)
99
100
  - **Output**: JSON representation of the component tree structure
100
101
 
101
102
  ## License
@@ -11,9 +11,13 @@ export const registerComponentTreeTool = (server) => {
11
11
  .number()
12
12
  .default(3)
13
13
  .describe('Maximum depth of the component tree (default: 3)'),
14
+ 'data-id': z
15
+ .string()
16
+ .optional()
17
+ .describe('Optional attribute name to capture as TreeNode.dataId (example: data-attribute-id)'),
14
18
  },
15
- }, async ({ entryFilePath, maxDepth }) => {
16
- const tree = getComponentTree(entryFilePath, maxDepth);
19
+ }, async ({ entryFilePath, maxDepth, ['data-id']: dataIdAttribute }) => {
20
+ const tree = getComponentTree(entryFilePath, maxDepth, dataIdAttribute);
17
21
  return {
18
22
  content: [
19
23
  {
@@ -24,7 +28,7 @@ export const registerComponentTreeTool = (server) => {
24
28
  };
25
29
  });
26
30
  };
27
- const getComponentTree = (entryFilePath, maxDepth) => {
31
+ const getComponentTree = (entryFilePath, maxDepth, dataIdAttribute) => {
28
32
  const project = createProject();
29
33
  const sourceFile = project.getSourceFile(entryFilePath);
30
34
  if (!sourceFile) {
@@ -33,5 +37,6 @@ const getComponentTree = (entryFilePath, maxDepth) => {
33
37
  const component = getComponent(sourceFile);
34
38
  return analyzeComponent(component, project, {
35
39
  maxDepth,
40
+ dataIdAttribute,
36
41
  }, 0);
37
42
  };
@@ -7,6 +7,7 @@ export const analyzeComponent = (node, project, options, currentDepth) => {
7
7
  name,
8
8
  type: 'component',
9
9
  children: [],
10
+ filePath: node.getSourceFile().getFilePath(),
10
11
  };
11
12
  if (currentDepth >= options.maxDepth) {
12
13
  return root;
@@ -1,16 +1,52 @@
1
- import { SyntaxKind } from 'ts-morph';
2
- import { getComponentName } from './nameHelper.js';
1
+ import { Node, SyntaxKind } from 'ts-morph';
2
+ const isComponentVariableDeclaration = (node) => {
3
+ if (!Node.isVariableDeclaration(node)) {
4
+ return false;
5
+ }
6
+ const initializer = node.getInitializer();
7
+ if (!initializer) {
8
+ return false;
9
+ }
10
+ return (initializer.getKind() === SyntaxKind.ArrowFunction ||
11
+ initializer.getKind() === SyntaxKind.FunctionExpression);
12
+ };
13
+ const isComponentDeclaration = (node) => {
14
+ if (Node.isFunctionDeclaration(node)) {
15
+ return true;
16
+ }
17
+ return isComponentVariableDeclaration(node);
18
+ };
19
+ const getFirstExportedComponent = (sourceFile) => {
20
+ const exportedDeclarations = sourceFile.getExportedDeclarations();
21
+ for (const declarations of exportedDeclarations.values()) {
22
+ const componentDeclaration = declarations.find(isComponentDeclaration);
23
+ if (componentDeclaration) {
24
+ return componentDeclaration;
25
+ }
26
+ }
27
+ return undefined;
28
+ };
3
29
  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];
30
+ const defaultExportDeclaration = sourceFile
31
+ .getDefaultExportSymbol()
32
+ ?.getDeclarations()
33
+ ?.find(isComponentDeclaration);
34
+ if (defaultExportDeclaration) {
35
+ return defaultExportDeclaration;
36
+ }
37
+ const firstExportedComponent = getFirstExportedComponent(sourceFile);
38
+ if (firstExportedComponent) {
39
+ return firstExportedComponent;
40
+ }
41
+ const localVariableComponent = sourceFile
42
+ .getVariableDeclarations()
43
+ .find(isComponentVariableDeclaration);
44
+ if (localVariableComponent) {
45
+ return localVariableComponent;
46
+ }
47
+ const localFunctionComponent = sourceFile.getFunctions()[0];
48
+ if (localFunctionComponent) {
49
+ return localFunctionComponent;
50
+ }
51
+ throw new Error(`Component not found in ${sourceFile.getFilePath()}`);
16
52
  };
@@ -2,8 +2,9 @@ import { Node, SyntaxKind, } from 'ts-morph';
2
2
  import { analyzeComponent } from './analyzeComponent.js';
3
3
  import { getComponent } from './component.js';
4
4
  import { getTagName } from './nameHelper.js';
5
+ import { extractPropsFromNode } from './props.js';
5
6
  import { handleTernary } from './ternary.js';
6
- const extractFromExpression = (expression, project, options, depth) => {
7
+ export const extractFromExpression = (expression, project, options, depth) => {
7
8
  if (Node.isConditionalExpression(expression)) {
8
9
  return handleTernary(expression, project, options, depth);
9
10
  }
@@ -15,7 +16,7 @@ const extractFromExpression = (expression, project, options, depth) => {
15
16
  }
16
17
  return extractJSX(expression, project, options, depth);
17
18
  };
18
- const handleLogicalAnd = (expression, project, options, depth) => {
19
+ export const handleLogicalAnd = (expression, project, options, depth) => {
19
20
  if (expression.getOperatorToken().getText() !== '&&') {
20
21
  return [];
21
22
  }
@@ -90,20 +91,65 @@ const resolveComponentFile = (node) => {
90
91
  const defaultImport = imp.getDefaultImport();
91
92
  if (defaultImport?.getText() === tagName) {
92
93
  const file = imp.getModuleSpecifierSourceFile();
93
- const component = getComponent(file);
94
- return component ?? null;
94
+ if (!file || file.getFilePath().includes('/node_modules/')) {
95
+ return null;
96
+ }
97
+ try {
98
+ const component = getComponent(file);
99
+ return component ?? null;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
95
104
  }
96
105
  for (const n of named) {
97
106
  if (n.getName() === tagName) {
98
107
  const file = imp.getModuleSpecifierSourceFile();
99
- const component = getComponent(file);
100
- return component ?? null;
108
+ if (!file || file.getFilePath().includes('/node_modules/')) {
109
+ return null;
110
+ }
111
+ try {
112
+ const component = getComponent(file);
113
+ return component ?? null;
114
+ }
115
+ catch {
116
+ return null;
117
+ }
101
118
  }
102
119
  }
103
120
  }
104
121
  return null;
105
122
  };
106
- function buildNodeFromJSX(node, project, options, depth) {
123
+ const resolveLocalComponentFilePath = (node) => {
124
+ const tagName = getTagName(node);
125
+ const sourceFile = node.getSourceFile();
126
+ for (const imp of sourceFile.getImportDeclarations()) {
127
+ const matchesDefaultImport = imp.getDefaultImport()?.getText() === tagName;
128
+ const matchesNamedImport = imp
129
+ .getNamedImports()
130
+ .some((namedImport) => namedImport.getName() === tagName);
131
+ if (!matchesDefaultImport && !matchesNamedImport) {
132
+ continue;
133
+ }
134
+ const moduleSpecifier = imp.getModuleSpecifierValue();
135
+ const isLocalImport = moduleSpecifier.startsWith('.') || moduleSpecifier.startsWith('/');
136
+ if (!isLocalImport) {
137
+ return undefined;
138
+ }
139
+ const importedSourceFile = imp.getModuleSpecifierSourceFile();
140
+ if (!importedSourceFile) {
141
+ return undefined;
142
+ }
143
+ const importedFilePath = importedSourceFile.getFilePath();
144
+ if (importedFilePath.includes('/node_modules/')) {
145
+ return undefined;
146
+ }
147
+ return importedFilePath;
148
+ }
149
+ // If there is no matching import, treat it as a component from the current local source file.
150
+ return sourceFile.getFilePath();
151
+ };
152
+ const buildNodeFromJSX = (node, project, options, depth) => {
107
153
  const tagName = getTagName(node);
108
154
  const isHtml = tagName[0] === tagName[0].toLowerCase();
109
155
  const treeNode = {
@@ -111,6 +157,22 @@ function buildNodeFromJSX(node, project, options, depth) {
111
157
  type: isHtml ? 'html' : 'component',
112
158
  children: [],
113
159
  };
160
+ if (!isHtml) {
161
+ const localComponentFilePath = resolveLocalComponentFilePath(node);
162
+ if (localComponentFilePath) {
163
+ treeNode.filePath = localComponentFilePath;
164
+ }
165
+ }
166
+ const props = extractPropsFromNode(node);
167
+ if (props) {
168
+ treeNode.props = props;
169
+ if (options.dataIdAttribute && options.dataIdAttribute in props) {
170
+ const dataIdValue = props[options.dataIdAttribute];
171
+ if (dataIdValue !== undefined && dataIdValue !== null) {
172
+ treeNode.dataId = String(dataIdValue);
173
+ }
174
+ }
175
+ }
114
176
  // Recurse into children
115
177
  if (Node.isJsxElement(node)) {
116
178
  const children = node.getJsxChildren();
@@ -140,12 +202,13 @@ function buildNodeFromJSX(node, project, options, depth) {
140
202
  if (!isHtml && depth < options.maxDepth) {
141
203
  const resolved = resolveComponentFile(node);
142
204
  if (resolved) {
205
+ treeNode.filePath = resolved.getSourceFile().getFilePath();
143
206
  const subTree = analyzeComponent(resolved, project, options, depth);
144
207
  treeNode.children.push(...subTree.children);
145
208
  }
146
209
  }
147
210
  return treeNode;
148
- }
211
+ };
149
212
  export const findReturnedJSX = (node) => {
150
213
  const returnStmt = node.getDescendantsOfKind(SyntaxKind.ReturnStatement)[0];
151
214
  if (returnStmt) {
@@ -0,0 +1,46 @@
1
+ import { Node, } from 'ts-morph';
2
+ const extractPropValue = (attribute) => {
3
+ const initializer = attribute.getInitializer();
4
+ if (!initializer) {
5
+ return true;
6
+ }
7
+ if (Node.isStringLiteral(initializer)) {
8
+ return initializer.getLiteralText();
9
+ }
10
+ if (Node.isJsxExpression(initializer)) {
11
+ const expression = initializer.getExpression();
12
+ if (!expression) {
13
+ return true;
14
+ }
15
+ return expression.getText();
16
+ }
17
+ return initializer.getText();
18
+ };
19
+ const addSpreadProps = (props, spreadAttribute) => {
20
+ const expression = spreadAttribute.getExpression();
21
+ const expressionText = expression.getText();
22
+ // Keep spread attributes as references so callers can resolve them later if needed.
23
+ props[`...${expressionText}`] = expressionText;
24
+ };
25
+ export const extractPropsFromNode = (node) => {
26
+ const attributes = Node.isJsxElement(node)
27
+ ? node.getOpeningElement().getAttributes()
28
+ : node.getAttributes();
29
+ if (attributes.length === 0) {
30
+ return undefined;
31
+ }
32
+ const props = {};
33
+ for (const attribute of attributes) {
34
+ if (Node.isJsxAttribute(attribute)) {
35
+ props[attribute.getNameNode().getText()] = extractPropValue(attribute);
36
+ continue;
37
+ }
38
+ if (Node.isJsxSpreadAttribute(attribute)) {
39
+ addSpreadProps(props, attribute);
40
+ }
41
+ }
42
+ if (Object.keys(props).length === 0) {
43
+ return undefined;
44
+ }
45
+ return props;
46
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsc-typescript-ast-mcp",
3
- "version": "1.0.11",
3
+ "version": "1.1.1",
4
4
  "mcpName": "io.github.jscoobyced/jsc-typescript-ast-mcp",
5
5
  "description": "A Model Context Protocol (MCP) server that provides an abstract syntax tree (AST) representation of TypeScript code using the ts-morph library. It allows clients to analyze and manipulate TypeScript code structures, making it easier for AI models to understand and work with TypeScript projects. You can create a JSON representation of your React components, and use it for various purposes such as documentation, code analysis, or even generating new code based on existing components.",
6
6
  "main": "dist/index.js",
@@ -21,6 +21,7 @@
21
21
  "scripts": {
22
22
  "format": "prettier --write .",
23
23
  "build": "tsc",
24
+ "lint": "eslint . --ext .ts",
24
25
  "test:mcp": "npx @modelcontextprotocol/inspector node dist/index.js"
25
26
  },
26
27
  "bin": {