Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions src/mcp-ts-introspect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# TypeScript Introspect Tool

A command-line tool for introspecting TypeScript exports from packages, source code, or projects. Can also run as an MCP (Model Context Protocol) server.

Forked from https://github.com/t3ta/ts-introspect-mcp-server/tree/master/src

## Usage

```bash
tools mcp-ts-introspect [options]
```

## Modes

The tool supports three introspection modes:

### 1. Package Mode

Introspect TypeScript exports from npm packages:

```bash
tools mcp-ts-introspect -m package -p typescript -t "Type.*"
tools mcp-ts-introspect -m package -p @types/node --limit 20
```

### 2. Source Mode

Analyze TypeScript source code directly:

```bash
tools mcp-ts-introspect -m source -s "export function hello() { return 'world'; }"
```

### 3. Project Mode

Analyze an entire TypeScript project:

```bash
tools mcp-ts-introspect -m project --project ./my-project
tools mcp-ts-introspect -m project --search-term "^get" --limit 20
```

## Options

- `-m, --mode MODE` - Introspection mode: package, source, or project
- `-p, --package NAME` - Package name to introspect (for package mode)
- `-s, --source CODE` - TypeScript source code to analyze (for source mode)
- `--project PATH` - Project path to analyze (for project mode, defaults to current directory)
- `--search-paths PATH` - Additional paths to search for packages (can use multiple times)
- `-t, --search-term TERM` - Filter exports by search term (supports regex)
- `--cache` - Enable caching (default: true)
- `--cache-dir DIR` - Cache directory (default: .ts-morph-cache)
- `--limit NUM` - Maximum number of results to return
- `-o, --output DEST` - Output destination: file, clipboard, or stdout (default: stdout)
- `-v, --verbose` - Enable verbose logging
- `-h, --help` - Show help message
- `--mcp` - Run as MCP server

## Examples

### Interactive Mode

Run without arguments for interactive prompts:

```bash
tools mcp-ts-introspect
```

### Find specific exports in a package

```bash
tools mcp-ts-introspect -m package -p typescript -t "^create" --limit 10
```

### Analyze source code and copy to clipboard

```bash
tools mcp-ts-introspect -m source -s "$(cat myfile.ts)" -o clipboard
```

### Analyze current project

```bash
tools mcp-ts-introspect -m project --search-term "Controller$" -o exports.json
```

## Features

- **Package Resolution**: Supports npm, yarn, and pnpm package managers
- **Caching**: Speeds up repeated lookups with file-based caching
- **Filtering**: Use regex patterns to filter exports by name, type, or description
- **Multiple Output Formats**: Output to stdout, clipboard, or file
- **JSDoc Support**: Extracts descriptions from JSDoc comments
- **TypeScript Support**: Full TypeScript type information extraction

## MCP Server Mode

Run the tool as an MCP server to integrate with AI assistants:

```bash
tools mcp-ts-introspect --mcp
```

### MCP Configuration

Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
"mcpServers": {
"ts-introspect": {
"command": "tools",
"args": ["mcp-ts-introspect", "--mcp"]
Comment on lines +112 to +113
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The MCP configuration example here uses "command": "tools", which might not work reliably with some clients like Claude Desktop. The main README.md correctly notes that a full, absolute path to the command is often required. It would be beneficial to update this example to reflect that best practice for a better user experience.

Suggested change
"command": "tools",
"args": ["mcp-ts-introspect", "--mcp"]
"command": "/path/to/GenesisTools/tools",
"args": ["mcp-ts-introspect", "--mcp"]

}
}
}
```

### Available MCP Tools

When running as an MCP server, the following tools are available:

1. **introspect-package** - Introspect TypeScript exports from an npm package

- `packageName` (required): The npm package name
- `searchPaths`: Additional search paths
- `searchTerm`: Regex filter pattern
- `cache`: Enable caching (default: true)
- `cacheDir`: Cache directory
- `limit`: Maximum results

2. **introspect-source** - Analyze TypeScript source code

- `sourceCode` (required): TypeScript source to analyze
- `searchTerm`: Regex filter pattern
- `limit`: Maximum results

3. **introspect-project** - Analyze a TypeScript project
- `projectPath`: Path to project (defaults to current directory)
- `searchTerm`: Regex filter pattern
- `cache`: Enable caching (default: true)
- `cacheDir`: Cache directory
- `limit`: Maximum results

## Notes

- The tool requires TypeScript declaration files (.d.ts) for package introspection
- Caching is enabled by default and stores results for 7 days
- Use verbose mode (-v) for debugging and additional logging
- When running as MCP server, logs are written to the GenesisTools log directory
53 changes: 53 additions & 0 deletions src/mcp-ts-introspect/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { existsSync } from "node:fs";
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import logger from "../logger";
import type { CacheEntry, ExportInfo } from "./types";

const CACHE_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days

export async function loadCache(cacheDir: string, key: string): Promise<ExportInfo[] | null> {
const cacheFile = join(cacheDir, `${key}.json`);

if (!existsSync(cacheFile)) {
return null;
}

try {
const cacheData = (await Bun.file(cacheFile).json()) as CacheEntry;

// Check if cache is still valid
const age = Date.now() - cacheData.timestamp;
if (age > CACHE_TTL) {
logger.info(`Cache for ${key} is expired (${Math.floor(age / 1000 / 60 / 60)} hours old)`);
return null;
}
Comment on lines +16 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add validation for cache data structure.

The as CacheEntry assertion trusts that the JSON file contains valid data. Corrupted or tampered cache files could cause runtime errors when accessing cacheData.exports or cacheData.timestamp.

🛡️ Proposed fix with validation
     try {
-        const cacheData = (await Bun.file(cacheFile).json()) as CacheEntry;
+        const cacheData = await Bun.file(cacheFile).json();
+
+        // Validate cache structure
+        if (!cacheData || typeof cacheData.timestamp !== "number" || !Array.isArray(cacheData.exports)) {
+            logger.warn(`Invalid cache structure for ${key}`);
+            return null;
+        }

         // Check if cache is still valid
         const age = Date.now() - cacheData.timestamp;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const cacheData = (await Bun.file(cacheFile).json()) as CacheEntry;
// Check if cache is still valid
const age = Date.now() - cacheData.timestamp;
if (age > CACHE_TTL) {
logger.info(`Cache for ${key} is expired (${Math.floor(age / 1000 / 60 / 60)} hours old)`);
return null;
}
try {
const cacheData = await Bun.file(cacheFile).json();
// Validate cache structure
if (!cacheData || typeof cacheData.timestamp !== "number" || !Array.isArray(cacheData.exports)) {
logger.warn(`Invalid cache structure for ${key}`);
return null;
}
// Check if cache is still valid
const age = Date.now() - cacheData.timestamp;
if (age > CACHE_TTL) {
logger.info(`Cache for ${key} is expired (${Math.floor(age / 1000 / 60 / 60)} hours old)`);
return null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp-ts-introspect/cache.ts` around lines 16 - 24, Validate the parsed
JSON before trusting the cast to CacheEntry: after reading cacheFile via
Bun.file(cacheFile).json(), check that the result is a non-null object, has a
numeric timestamp and an array exports (e.g., typeof cacheData === 'object' &&
cacheData !== null && typeof cacheData.timestamp === 'number' &&
Array.isArray(cacheData.exports')); if validation fails, log a warning with
logger (including key) and return null instead of proceeding to compute age or
access cacheData.exports; keep using CACHE_TTL and the existing try/catch so
corrupted or tampered files are safely ignored.


logger.info(`Loaded cache for ${key} (${Math.floor(age / 1000 / 60)} minutes old)`);
return cacheData.exports;
} catch (error) {
logger.warn(`Failed to load cache for ${key}: ${error}`);
return null;
}
}

export async function saveCache(cacheDir: string, key: string, exports: ExportInfo[]): Promise<void> {
try {
// Ensure cache directory exists
if (!existsSync(cacheDir)) {
await mkdir(cacheDir, { recursive: true });
}

const cacheFile = join(cacheDir, `${key}.json`);
const cacheEntry: CacheEntry = {
exports,
timestamp: Date.now(),
};

await Bun.write(cacheFile, JSON.stringify(cacheEntry, null, 2));
logger.info(`Saved cache for ${key} (${exports.length} exports)`);
} catch (error) {
logger.warn(`Failed to save cache for ${key}: ${error}`);
// Don't throw - caching is optional
}
}
110 changes: 110 additions & 0 deletions src/mcp-ts-introspect/exportExtractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { type Symbol as MorphSymbol, Node, type SourceFile } from "ts-morph";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if ts-morph is declared in package.json
echo "=== Checking package.json for ts-morph dependency ==="
cat package.json | jq '.dependencies["ts-morph"] // .devDependencies["ts-morph"] // "NOT FOUND"'

Repository: genesiscz/GenesisTools

Length of output: 131


Add ts-morph to package.json dependencies.

The file imports ts-morph but the package is missing from package.json, causing CI failures. Add it to your dependencies.

🧰 Tools
🪛 GitHub Actions: CI

[error] 1-1: Cannot find module 'ts-morph' or its corresponding type declarations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp-ts-introspect/exportExtractor.ts` at line 1, The project imports
ts-morph (see import of type Symbol as MorphSymbol, Node, SourceFile in
exportExtractor.ts) but ts-morph is missing from package.json; add ts-morph to
dependencies (e.g., run npm install --save ts-morph or add an appropriate
version entry under "dependencies") so CI can resolve the import and build
succeeds.

import logger from "../logger";
import type { ExportInfo } from "./types";

export async function extractExports(sourceFile: SourceFile): Promise<ExportInfo[]> {
const exports: ExportInfo[] = [];
const exportedSymbols = sourceFile.getExportSymbols();

logger.info(`Extracting exports from ${sourceFile.getFilePath()}, found ${exportedSymbols.length} export symbols`);

for (const symbol of exportedSymbols) {
const exportInfo = processSymbol(symbol);
if (exportInfo) {
exports.push(exportInfo);
}
}

return exports;
}
Comment on lines +5 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

async keyword is unnecessary.

extractExports contains no await expressions and could be a regular synchronous function. The async keyword adds minor overhead by wrapping the return in a Promise.

♻️ Proposed refactor
-export async function extractExports(sourceFile: SourceFile): Promise<ExportInfo[]> {
+export function extractExports(sourceFile: SourceFile): ExportInfo[] {

Note: If you change this, callers in introspect.ts that use withTimeout(extractExports(...)) would need adjustment since withTimeout expects a Promise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp-ts-introspect/exportExtractor.ts` around lines 5 - 19, The function
extractExports is declared async but contains no awaits; remove the async
keyword and make its signature synchronous (export function
extractExports(sourceFile: SourceFile): ExportInfo[]) and return the exports
array directly; then update callers that rely on a Promise (notably usages in
introspect.ts that call withTimeout(extractExports(...))) to either wrap the
result with Promise.resolve(extractExports(...)) or call withTimeoutSync/adjust
withTimeout to accept synchronous results so the call sites remain correct.


function processSymbol(symbol: MorphSymbol): ExportInfo | null {
const name = symbol.getName();

// Skip default exports and internal symbols
if (name === "default" || name.startsWith("_")) {
return null;
}

const declarations = symbol.getDeclarations();
if (declarations.length === 0) {
return null;
}

const declaration = declarations[0];
const node = declaration as Node;
Comment on lines +34 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Redundant type assertion.

declarations[0] from symbol.getDeclarations() already returns a Node subtype. The intermediate declaration variable and the cast to Node are unnecessary.

♻️ Proposed simplification
     const declaration = declarations[0];
-    const node = declaration as Node;

     // Get type signature
-    const type = symbol.getTypeAtLocation(node);
-    const typeSignature = type.getText(node);
+    const type = symbol.getTypeAtLocation(declaration);
+    const typeSignature = type.getText(declaration);

     // Get JSDoc description
-    const description = getDescription(node);
+    const description = getDescription(declaration);

     // Determine kind
-    const kind = getExportKind(node);
+    const kind = getExportKind(declaration);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const declaration = declarations[0];
const node = declaration as Node;
const declaration = declarations[0];
// Get type signature
const type = symbol.getTypeAtLocation(declaration);
const typeSignature = type.getText(declaration);
// Get JSDoc description
const description = getDescription(declaration);
// Determine kind
const kind = getExportKind(declaration);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp-ts-introspect/exportExtractor.ts` around lines 34 - 35, The code
creates an unnecessary intermediate variable and a redundant cast: remove the
local const declaration and the `as Node` cast by using the declaration directly
from `symbol.getDeclarations()` where needed (e.g., replace uses of
`declaration`/`node` with the first element of `declarations` or assign it once
without a cast). Update references in exportExtractor.ts around the
`declarations[0]` usage (the block that currently declares `const declaration =
declarations[0]; const node = declaration as Node;`) to use the declaration
value directly with its inferred Node subtype.


// Get type signature
const type = symbol.getTypeAtLocation(node);
const typeSignature = type.getText(node);

// Get JSDoc description
const description = getDescription(node);

// Determine kind
const kind = getExportKind(node);
if (!kind) {
return null;
}

return {
name,
kind,
typeSignature,
description,
};
}

function getExportKind(node: Node): ExportInfo["kind"] | null {
if (Node.isTypeAliasDeclaration(node)) {
return "type";
} else if (Node.isFunctionDeclaration(node)) {
return "function";
} else if (Node.isClassDeclaration(node)) {
return "class";
} else if (Node.isVariableDeclaration(node)) {
return "const";
} else if (Node.isExportSpecifier(node)) {
// For re-exports, check the original declaration
const symbol = node.getSymbol();
if (symbol) {
const valueDeclaration = symbol.getValueDeclaration();
if (valueDeclaration) {
return getExportKind(valueDeclaration);
}
}
}

// Default to const for other types
return "const";
}
Comment on lines +58 to +80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing InterfaceDeclaration handling.

Exported interfaces will fall through to the default "const" return, which is incorrect. Consider adding a case for Node.isInterfaceDeclaration(node) returning "type".

🐛 Proposed fix
 function getExportKind(node: Node): ExportInfo["kind"] | null {
     if (Node.isTypeAliasDeclaration(node)) {
         return "type";
+    } else if (Node.isInterfaceDeclaration(node)) {
+        return "type";
     } else if (Node.isFunctionDeclaration(node)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getExportKind(node: Node): ExportInfo["kind"] | null {
if (Node.isTypeAliasDeclaration(node)) {
return "type";
} else if (Node.isFunctionDeclaration(node)) {
return "function";
} else if (Node.isClassDeclaration(node)) {
return "class";
} else if (Node.isVariableDeclaration(node)) {
return "const";
} else if (Node.isExportSpecifier(node)) {
// For re-exports, check the original declaration
const symbol = node.getSymbol();
if (symbol) {
const valueDeclaration = symbol.getValueDeclaration();
if (valueDeclaration) {
return getExportKind(valueDeclaration);
}
}
}
// Default to const for other types
return "const";
}
function getExportKind(node: Node): ExportInfo["kind"] | null {
if (Node.isTypeAliasDeclaration(node)) {
return "type";
} else if (Node.isInterfaceDeclaration(node)) {
return "type";
} else if (Node.isFunctionDeclaration(node)) {
return "function";
} else if (Node.isClassDeclaration(node)) {
return "class";
} else if (Node.isVariableDeclaration(node)) {
return "const";
} else if (Node.isExportSpecifier(node)) {
// For re-exports, check the original declaration
const symbol = node.getSymbol();
if (symbol) {
const valueDeclaration = symbol.getValueDeclaration();
if (valueDeclaration) {
return getExportKind(valueDeclaration);
}
}
}
// Default to const for other types
return "const";
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp-ts-introspect/exportExtractor.ts` around lines 58 - 80, getExportKind
is missing handling for exported interfaces so InterfaceDeclaration nodes fall
through to the default "const"; update getExportKind to check for
Node.isInterfaceDeclaration(node) and return "type" (similar to the TypeAlias
case), and also ensure the re-export branch (inside Node.isExportSpecifier) will
recurse correctly when symbol.getValueDeclaration() returns an
InterfaceDeclaration so the added check covers those paths as well.


function getDescription(node: Node): string | null {
// Try to get JSDoc comments
if (!("getJsDocs" in node)) {
return null;
}

const jsDocs = (node as unknown as { getJsDocs(): Array<{ getDescription(): string }> }).getJsDocs();

for (const jsDoc of jsDocs) {
const description = jsDoc.getDescription();
if (description) {
return description.trim();
}
}

// Check parent node for JSDoc (useful for variable declarations)
const parent = node.getParent();
if (parent && "getJsDocs" in parent) {
const parentJsDocs = (parent as unknown as { getJsDocs(): Array<{ getDescription(): string }> }).getJsDocs();
for (const jsDoc of parentJsDocs) {
const description = jsDoc.getDescription();
if (description) {
return description.trim();
}
}
}

return null;
}
Loading
Loading