jsc-typescript-ast-mcp 1.1.6 → 1.1.8

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
@@ -50,7 +50,7 @@ To use this MCP server in Claude Code:
50
50
  "typescript-ast"
51
51
  ],
52
52
  "env": {
53
- "PROJECT_TSCONFIG_PATH": "PATH/TO/YOUR/TYPESCRIPT?PROJECT/tsconfig.json"
53
+ "PROJECT_TSCONFIG_PATH": "PATH/TO/YOUR/TYPESCRIPT/PROJECT/tsconfig.json"
54
54
  }
55
55
  }
56
56
  }
package/dist/main.js ADDED
@@ -0,0 +1,9 @@
1
+ import path from 'path';
2
+ import { getComponentTree } from './ts-morph/componentTree.js';
3
+ const rootDir = __dirname === 'dist'
4
+ ? path.resolve(__dirname, '../../')
5
+ : __dirname === 'code'
6
+ ? path.resolve(__dirname, '../')
7
+ : __dirname;
8
+ const testApp = getComponentTree(path.resolve(rootDir, 'sample-app/src/App.tsx'), 5, 'my-data-id');
9
+ console.log(JSON.stringify(testApp, null, 2));
@@ -28,7 +28,7 @@ export const registerComponentTreeTool = (server) => {
28
28
  };
29
29
  });
30
30
  };
31
- const getComponentTree = (entryFilePath, maxDepth, dataIdAttribute) => {
31
+ export const getComponentTree = (entryFilePath, maxDepth, dataIdAttribute) => {
32
32
  const project = createProject();
33
33
  const sourceFile = project.getSourceFile(entryFilePath);
34
34
  if (!sourceFile) {
@@ -2,7 +2,7 @@ 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 { extractAttributeValueFromNode, extractOnClickInfoFromNode, extractPropsFromNode, } from './props.js';
5
+ import { extractAttributeValueFromNode, extractCtaInfoFromNode, extractPropsFromNode, } from './props.js';
6
6
  import { handleTernary } from './ternary.js';
7
7
  export const extractFromExpression = (expression, project, options, depth) => {
8
8
  if (Node.isConditionalExpression(expression)) {
@@ -42,7 +42,9 @@ const extractFromIdentifier = (expression, project, options, depth) => {
42
42
  const buildNodeFromJsxFragment = (node, project, options, depth) => ({
43
43
  name: 'Fragment',
44
44
  type: 'fragment',
45
- children: extractJSX(node, project, options, depth),
45
+ children: depth < options.maxDepth
46
+ ? extractJSX(node, project, options, depth + 1)
47
+ : [],
46
48
  });
47
49
  export const extractJSX = (node, project, options, depth) => {
48
50
  const results = [];
@@ -173,33 +175,33 @@ const buildNodeFromJSX = (node, project, options, depth) => {
173
175
  treeNode.dataId = String(dataIdValue);
174
176
  }
175
177
  }
176
- const onClick = extractOnClickInfoFromNode(node);
177
- if (onClick) {
178
- treeNode.onClick = onClick;
178
+ const cta = extractCtaInfoFromNode(node);
179
+ if (cta) {
180
+ treeNode.cta = cta;
179
181
  }
180
- // Recurse into children
181
- if (Node.isJsxElement(node)) {
182
+ // Recurse into children while under maxDepth.
183
+ if (Node.isJsxElement(node) && depth < options.maxDepth) {
182
184
  const children = node.getJsxChildren();
183
185
  children.forEach((child) => {
184
186
  if (Node.isJsxElement(child) || Node.isJsxSelfClosingElement(child)) {
185
- const childNode = buildNodeFromJSX(child, project, options, depth);
187
+ const childNode = buildNodeFromJSX(child, project, options, depth + 1);
186
188
  if (childNode) {
187
189
  treeNode.children.push(childNode);
188
190
  }
189
191
  return;
190
192
  }
191
193
  if (Node.isJsxFragment(child)) {
192
- treeNode.children.push(buildNodeFromJsxFragment(child, project, options, depth));
194
+ treeNode.children.push(buildNodeFromJsxFragment(child, project, options, depth + 1));
193
195
  return;
194
196
  }
195
197
  if (Node.isJsxExpression(child)) {
196
198
  const expression = child.getExpression();
197
199
  if (expression) {
198
- treeNode.children.push(...extractFromExpression(expression, project, options, depth));
200
+ treeNode.children.push(...extractFromExpression(expression, project, options, depth + 1));
199
201
  }
200
202
  return;
201
203
  }
202
- treeNode.children.push(...extractJSX(child, project, options, depth));
204
+ treeNode.children.push(...extractJSX(child, project, options, depth + 1));
203
205
  });
204
206
  }
205
207
  // Resolve component if custom
@@ -214,9 +216,31 @@ const buildNodeFromJSX = (node, project, options, depth) => {
214
216
  return treeNode;
215
217
  };
216
218
  export const findReturnedJSX = (node) => {
217
- const returnStmt = node.getDescendantsOfKind(SyntaxKind.ReturnStatement)[0];
218
- if (returnStmt) {
219
- return returnStmt.getExpression() ?? null;
219
+ const getReturnedJsxExpression = (expression) => {
220
+ if (Node.isParenthesizedExpression(expression)) {
221
+ const innerExpression = expression.getExpression();
222
+ if (!innerExpression) {
223
+ return null;
224
+ }
225
+ return getReturnedJsxExpression(innerExpression);
226
+ }
227
+ if (Node.isJsxElement(expression) ||
228
+ Node.isJsxSelfClosingElement(expression) ||
229
+ Node.isJsxFragment(expression)) {
230
+ return expression;
231
+ }
232
+ return null;
233
+ };
234
+ const returnStatements = node.getDescendantsOfKind(SyntaxKind.ReturnStatement);
235
+ for (const returnStmt of returnStatements) {
236
+ const expression = returnStmt.getExpression();
237
+ if (!expression) {
238
+ continue;
239
+ }
240
+ const returnedJsxExpression = getReturnedJsxExpression(expression);
241
+ if (returnedJsxExpression) {
242
+ return returnedJsxExpression;
243
+ }
220
244
  }
221
245
  // Arrow function implicit return
222
246
  if (Node.isArrowFunction(node)) {
@@ -1,11 +1,11 @@
1
1
  import { Node, } from 'ts-morph';
2
- const STANDARD_HTML_ATTRIBUTES = new Set([
2
+ const EXCLUDE_HTML_ATTRIBUTES = new Set([
3
3
  'accept',
4
4
  'acceptcharset',
5
- 'action',
5
+ // 'action',
6
6
  'allow',
7
7
  'allowfullscreen',
8
- 'alt',
8
+ // 'alt',
9
9
  'as',
10
10
  'async',
11
11
  'autocapitalize',
@@ -18,30 +18,30 @@ const STANDARD_HTML_ATTRIBUTES = new Set([
18
18
  'cellspacing',
19
19
  'charset',
20
20
  'challenge',
21
- 'checked',
21
+ // 'checked',
22
22
  'cite',
23
23
  'class',
24
24
  'classname',
25
25
  'cols',
26
26
  'colspan',
27
- 'content',
27
+ // 'content',
28
28
  'contenteditable',
29
29
  'controls',
30
30
  'coords',
31
31
  'crossorigin',
32
- 'data',
33
- 'datetime',
32
+ // 'data',
33
+ // 'datetime',
34
34
  'decoding',
35
35
  'default',
36
36
  'defer',
37
37
  'dir',
38
38
  'dirname',
39
- 'disabled',
39
+ // 'disabled',
40
40
  'download',
41
41
  'draggable',
42
42
  'enctype',
43
43
  'fetchpriority',
44
- 'for',
44
+ // 'for',
45
45
  'form',
46
46
  'formaction',
47
47
  'formenctype',
@@ -50,20 +50,20 @@ const STANDARD_HTML_ATTRIBUTES = new Set([
50
50
  'formtarget',
51
51
  'frameborder',
52
52
  'headers',
53
- 'height',
54
- 'hidden',
53
+ // 'height',
54
+ // 'hidden',
55
55
  'high',
56
56
  'href',
57
57
  'hreflang',
58
58
  'htmlfor',
59
59
  'httpequiv',
60
- 'id',
60
+ // 'id',
61
61
  'inputmode',
62
62
  'integrity',
63
63
  'is',
64
- 'key',
64
+ // 'key',
65
65
  'kind',
66
- 'label',
66
+ // 'label',
67
67
  'lang',
68
68
  'list',
69
69
  'loading',
@@ -77,23 +77,23 @@ const STANDARD_HTML_ATTRIBUTES = new Set([
77
77
  'minlength',
78
78
  'multiple',
79
79
  'muted',
80
- 'name',
80
+ // 'name',
81
81
  'nomodule',
82
82
  'nonce',
83
83
  'novalidate',
84
84
  'open',
85
85
  'optimum',
86
86
  'pattern',
87
- 'placeholder',
87
+ // 'placeholder',
88
88
  'playsinline',
89
89
  'poster',
90
90
  'preload',
91
- 'readonly',
91
+ // 'readonly',
92
92
  'referrerpolicy',
93
- 'rel',
93
+ // 'rel',
94
94
  'required',
95
95
  'reversed',
96
- 'role',
96
+ // 'role',
97
97
  'rows',
98
98
  'rowspan',
99
99
  'sandbox',
@@ -114,12 +114,12 @@ const STANDARD_HTML_ATTRIBUTES = new Set([
114
114
  'style',
115
115
  'tabindex',
116
116
  'target',
117
- 'title',
117
+ // 'title',
118
118
  'translate',
119
119
  'type',
120
120
  'usemap',
121
- 'value',
122
- 'width',
121
+ // 'value',
122
+ // 'width',
123
123
  'wrap',
124
124
  // Common DOM event handler props
125
125
  'onabort',
@@ -204,13 +204,22 @@ const STANDARD_HTML_ATTRIBUTES = new Set([
204
204
  'onwaiting',
205
205
  'onwheel',
206
206
  ]);
207
+ const CTA_EVENTS = new Set([
208
+ 'onClick',
209
+ 'onChange',
210
+ 'onInput',
211
+ 'onSubmit',
212
+ 'onSelect',
213
+ 'onKeyDown',
214
+ 'onKeyUp',
215
+ 'onBlur',
216
+ ]);
207
217
  const isStandardHtmlAttribute = (name) => {
208
218
  const normalizedName = name.toLowerCase();
209
- if (normalizedName.startsWith('aria-') ||
210
- normalizedName.startsWith('data-')) {
219
+ if (normalizedName.startsWith('aria-')) {
211
220
  return true;
212
221
  }
213
- return STANDARD_HTML_ATTRIBUTES.has(normalizedName);
222
+ return EXCLUDE_HTML_ATTRIBUTES.has(normalizedName);
214
223
  };
215
224
  const normalizeAttributeName = (name) => name.replace(/[-_]/g, '').toLowerCase();
216
225
  const matchesAttributeName = (name, target) => normalizeAttributeName(name) === normalizeAttributeName(target);
@@ -294,26 +303,27 @@ const getExpressionKind = (expression) => {
294
303
  }
295
304
  return 'expression';
296
305
  };
297
- export const extractOnClickInfoFromNode = (node) => {
306
+ export const extractCtaInfoFromNode = (node) => {
298
307
  const attributes = Node.isJsxElement(node)
299
308
  ? node.getOpeningElement().getAttributes()
300
309
  : node.getAttributes();
301
- const onClickAttribute = attributes.find((attribute) => Node.isJsxAttribute(attribute) &&
302
- attribute.getNameNode().getText() === 'onClick');
303
- if (!onClickAttribute || !Node.isJsxAttribute(onClickAttribute)) {
310
+ const ctaAttribute = attributes.find((attribute) => Node.isJsxAttribute(attribute) &&
311
+ CTA_EVENTS.has(attribute.getNameNode().getText()));
312
+ if (!ctaAttribute || !Node.isJsxAttribute(ctaAttribute)) {
304
313
  return undefined;
305
314
  }
306
- const initializer = onClickAttribute.getInitializer();
315
+ const eventAttributeName = ctaAttribute.getNameNode().getText();
316
+ const initializer = ctaAttribute.getInitializer();
307
317
  if (!initializer) {
308
318
  return {
309
- attribute: 'onClick',
319
+ attribute: eventAttributeName,
310
320
  expression: 'true',
311
321
  kind: 'boolean-shorthand',
312
322
  };
313
323
  }
314
324
  if (Node.isStringLiteral(initializer)) {
315
325
  return {
316
- attribute: 'onClick',
326
+ attribute: eventAttributeName,
317
327
  expression: initializer.getLiteralText(),
318
328
  kind: 'string-literal',
319
329
  };
@@ -324,13 +334,13 @@ export const extractOnClickInfoFromNode = (node) => {
324
334
  return undefined;
325
335
  }
326
336
  return {
327
- attribute: 'onClick',
337
+ attribute: eventAttributeName,
328
338
  expression: expression.getText(),
329
339
  kind: getExpressionKind(expression),
330
340
  };
331
341
  }
332
342
  return {
333
- attribute: 'onClick',
343
+ attribute: eventAttributeName,
334
344
  expression: initializer.getText(),
335
345
  kind: 'expression',
336
346
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsc-typescript-ast-mcp",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
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",