jsc-typescript-ast-mcp 1.0.11 → 1.1.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/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;
@@ -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
  }
@@ -103,7 +104,7 @@ const resolveComponentFile = (node) => {
103
104
  }
104
105
  return null;
105
106
  };
106
- function buildNodeFromJSX(node, project, options, depth) {
107
+ const buildNodeFromJSX = (node, project, options, depth) => {
107
108
  const tagName = getTagName(node);
108
109
  const isHtml = tagName[0] === tagName[0].toLowerCase();
109
110
  const treeNode = {
@@ -111,6 +112,19 @@ function buildNodeFromJSX(node, project, options, depth) {
111
112
  type: isHtml ? 'html' : 'component',
112
113
  children: [],
113
114
  };
115
+ if (!isHtml) {
116
+ treeNode.filePath = node.getSourceFile().getFilePath();
117
+ }
118
+ const props = extractPropsFromNode(node);
119
+ if (props) {
120
+ treeNode.props = props;
121
+ if (options.dataIdAttribute && options.dataIdAttribute in props) {
122
+ const dataIdValue = props[options.dataIdAttribute];
123
+ if (dataIdValue !== undefined && dataIdValue !== null) {
124
+ treeNode.dataId = String(dataIdValue);
125
+ }
126
+ }
127
+ }
114
128
  // Recurse into children
115
129
  if (Node.isJsxElement(node)) {
116
130
  const children = node.getJsxChildren();
@@ -140,12 +154,13 @@ function buildNodeFromJSX(node, project, options, depth) {
140
154
  if (!isHtml && depth < options.maxDepth) {
141
155
  const resolved = resolveComponentFile(node);
142
156
  if (resolved) {
157
+ treeNode.filePath = resolved.getSourceFile().getFilePath();
143
158
  const subTree = analyzeComponent(resolved, project, options, depth);
144
159
  treeNode.children.push(...subTree.children);
145
160
  }
146
161
  }
147
162
  return treeNode;
148
- }
163
+ };
149
164
  export const findReturnedJSX = (node) => {
150
165
  const returnStmt = node.getDescendantsOfKind(SyntaxKind.ReturnStatement)[0];
151
166
  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.0",
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": {