Add mcp-ts-introspect: TypeScript package introspection tool#18
Add mcp-ts-introspect: TypeScript package introspection tool#18
Conversation
📝 WalkthroughWalkthroughThis PR introduces a complete TypeScript introspection tool ( Changes
Sequence DiagramsequenceDiagram
participant User as User/CLI
participant Index as CLI Handler<br/>(index.ts)
participant Resolver as PackageResolver<br/>(packageResolver.ts)
participant Introspect as Introspection<br/>(introspect.ts)
participant Extractor as ExportExtractor<br/>(exportExtractor.ts)
participant Cache as Cache<br/>(cache.ts)
participant Output as Output Formatter<br/>(utils.ts)
User->>Index: CLI args (mode, package/source/path)
Index->>Index: Interactive prompts for missing inputs
Index->>Introspect: introspectPackage/Source/Project(...)
alt Package Mode
Introspect->>Resolver: findPackageJsonAndDir()
Resolver-->>Introspect: PackageLocation
Introspect->>Resolver: findDeclarationFiles()
Resolver-->>Introspect: .d.ts file paths
Introspect->>Cache: loadCache()
Cache-->>Introspect: cached exports or null
end
alt Cache Miss or Non-Package
Introspect->>Extractor: extractExports() per file
Extractor-->>Introspect: ExportInfo[]
Introspect->>Cache: saveCache()
end
Introspect->>Output: filterExports(searchTerm, limit)
Output-->>Introspect: filtered ExportInfo[]
Introspect-->>Index: ExportInfo[]
Index->>Index: formatExports (JSON/human-readable)
Index->>User: stdout/clipboard/file output
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
Summary of ChangesHello @genesiscz, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a significant new capability to the GenesisTools suite: a TypeScript introspection tool. This tool empowers developers and AI assistants to deeply understand TypeScript codebases and packages by programmatically extracting detailed information about their exports, types, and structure. The addition of this tool is accompanied by a necessary cleanup and simplification of the project's main documentation, ensuring clarity and relevance. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a new and powerful TypeScript introspection tool, mcp-ts-introspect, which is a great addition to the GenesisTools suite. The implementation is robust, covering CLI, MCP server mode, caching, and various package manager quirks. The accompanying documentation is thorough. I've also noticed a significant cleanup of the main README.md, which improves its focus and readability. My review includes a few suggestions for code refinement, a bug fix for deduplication logic, and some minor documentation consistency improvements. Overall, this is a high-quality contribution.
src/mcp-ts-introspect/introspect.ts
Outdated
| const uniqueExports = Array.from( | ||
| new Map(allExports.map(exp => [exp.name, exp])).values() | ||
| ); |
There was a problem hiding this comment.
The current deduplication logic for package exports only considers the export name. This could lead to incorrect results if a package exports multiple items with the same name from different entry points (e.g., pkg/a and pkg/b). To ensure correctness, deduplication should be based on both the name and the type signature, similar to how it's done in introspectProject.
| const uniqueExports = Array.from( | |
| new Map(allExports.map(exp => [exp.name, exp])).values() | |
| ); | |
| const uniqueExports = Array.from( | |
| new Map(allExports.map(exp => [`${exp.name}:${exp.typeSignature}`, exp])).values() | |
| ); |
README.md
Outdated
| - `--all, -a` - All uncommitted files (default) | ||
| - `--commits, -c NUM` - Files from last NUM commits | ||
| - `--staged, -s` - Only staged files | ||
| - `--unstaged, -u` - Only unstaged files |
README.md
Outdated
| --- | ||
|
|
||
| ### 6. 🔄 Watch | ||
| ### 6. 🔄 Watch |
README.md
Outdated
|
|
||
| <div align="center"> | ||
|
|
||
| "command": "tools", | ||
| "args": ["mcp-ts-introspect", "--mcp"] |
There was a problem hiding this comment.
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.
| "command": "tools", | |
| "args": ["mcp-ts-introspect", "--mcp"] | |
| "command": "/path/to/GenesisTools/tools", | |
| "args": ["mcp-ts-introspect", "--mcp"] |
| function getDescription(node: Node): string | null { | ||
| // Try to get JSDoc comments | ||
| if (!('getJsDocs' in node)) { | ||
| return null; | ||
| } | ||
|
|
||
| const jsDocs = (node as any).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 any).getJsDocs(); | ||
| for (const jsDoc of parentJsDocs) { | ||
| const description = jsDoc.getDescription(); | ||
| if (description) { | ||
| return description.trim(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } No newline at end of file |
There was a problem hiding this comment.
The current implementation uses the in operator and any casts to check for and access getJsDocs. A more idiomatic and type-safe approach with ts-morph is to use the Node.isJSDocableNode() type guard. This avoids any and makes the code cleaner and more maintainable.
function getDescription(node: Node): string | null {
// Try to get JSDoc comments from the node itself
if (Node.isJSDocableNode(node)) {
for (const jsDoc of node.getJsDocs()) {
const description = jsDoc.getDescription();
if (description) {
return description.trim();
}
}
}
// Check parent node for JSDoc (useful for variable declarations)
const parent = node.getParent();
if (parent && Node.isJSDocableNode(parent)) {
for (const jsDoc of parent.getJsDocs()) {
const description = jsDoc.getDescription();
if (description) {
return description.trim();
}
}
}
return null;
}
src/mcp-ts-introspect/index.ts
Outdated
| searchPaths: Array.isArray(argv.searchPaths) ? argv.searchPaths : | ||
| argv.searchPaths ? [argv.searchPaths] : [], |
There was a problem hiding this comment.
There was a problem hiding this comment.
Pull request overview
This PR introduces a new mcp-ts-introspect tool to introspect TypeScript exports from npm packages, source strings, or whole projects, and exposes the same functionality via an MCP server. It also updates TypeScript compiler settings needed for CommonJS interop and refreshes documentation to include the new tool while removing outdated sections.
Changes:
- Add
mcp-ts-introspectCLI + interactive mode with package/source/project introspection. - Add MCP server mode exposing
introspect-package,introspect-source, andintrospect-projecttools. - Add caching + package resolution + export extraction utilities; update docs and
tsconfig.jsonfor interop.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Enables esModuleInterop and downlevelIteration for compatibility with new deps/import patterns. |
| src/mcp-ts-introspect/utils.ts | Adds export filtering (regex/string fallback) and result limiting. |
| src/mcp-ts-introspect/types.ts | Introduces shared types for export data, options, and cache entries. |
| src/mcp-ts-introspect/packageResolver.ts | Implements package discovery and .d.ts file location strategies (node_modules/pnpm/exports scanning). |
| src/mcp-ts-introspect/mcp-server.ts | Adds MCP server exposing introspection operations as tools over stdio transport. |
| src/mcp-ts-introspect/introspect.ts | Implements package/source/project introspection using ts-morph with caching and timeouts. |
| src/mcp-ts-introspect/index.ts | Adds CLI entrypoint: argument parsing, interactive prompts, output handling, and MCP mode. |
| src/mcp-ts-introspect/exportExtractor.ts | Extracts exported symbols and shapes them into ExportInfo records. |
| src/mcp-ts-introspect/cache.ts | Adds filesystem cache read/write with TTL semantics. |
| src/mcp-ts-introspect/README.md | Documents usage, options, examples, and MCP integration for the new tool. |
| README.md | Updates tool listing/docs and removes/condenses outdated sections while adding MCP TS Introspect. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (const entry of entries) { | ||
| if (entry.includes(packageName)) { | ||
| const packagePath = join(pnpmPath, entry, "node_modules", packageName); | ||
| const packageJsonPath = join(packagePath, "package.json"); | ||
|
|
||
| if (existsSync(packageJsonPath)) { |
There was a problem hiding this comment.
PNPM store folder names don’t include / for scoped packages (they use +, e.g. @types+node@...). Checking entry.includes(packageName) will fail for scoped packages like @types/node, so pnpm-installed scoped packages won’t be found. Consider matching against a normalized form (e.g., packageName.replace("/", "+")) and/or parsing the pnpm entry format more precisely to avoid false positives.
| 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"; | ||
| } |
There was a problem hiding this comment.
getExportKind() defaults to returning "const" for any unhandled declaration kinds. This will misclassify common exports from .d.ts such as interface, enum, and namespace as "const". Add explicit handling for these node kinds (and update ExportInfo["kind"] accordingly), or map them to a more accurate category (e.g., treat interfaces as "type").
| const timeout = new Promise<never>((_, reject) => { | ||
| setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs); | ||
| }); | ||
|
|
||
| return Promise.race([promise, timeout]); |
There was a problem hiding this comment.
withTimeout() creates a timer but never clears it when the main promise resolves. With many calls this can accumulate pending timers unnecessarily. Track the timer handle and clearTimeout() once either branch settles (e.g., in a finally around await Promise.race(...)).
| const timeout = new Promise<never>((_, reject) => { | |
| setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs); | |
| }); | |
| return Promise.race([promise, timeout]); | |
| let timeoutId: NodeJS.Timeout | undefined; | |
| const timeout = new Promise<never>((_, reject) => { | |
| timeoutId = setTimeout( | |
| () => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), | |
| timeoutMs | |
| ); | |
| }); | |
| try { | |
| return await Promise.race([promise, timeout]); | |
| } finally { | |
| if (timeoutId !== undefined) { | |
| clearTimeout(timeoutId); | |
| } | |
| } |
| if (request.params.name === "introspect-project") { | ||
| try { | ||
| const args = IntrospectProjectSchema.parse(request.params.arguments); | ||
| const options: IntrospectOptions = { |
There was a problem hiding this comment.
request.params.arguments may be undefined (especially for tools with no required params like introspect-project). Passing undefined to z.object(...).parse() will throw a ZodError even though {} would be valid. Consider parsing request.params.arguments ?? {} for schemas where arguments are optional.
src/mcp-ts-introspect/index.ts
Outdated
| } | ||
|
|
||
| async function getOutputDestination(argv: Args): Promise<string> { | ||
| if (argv.output) return argv.output; |
There was a problem hiding this comment.
The CLI help says --output accepts file, but getOutputDestination() returns argv.output verbatim. Passing --output file will write to a file literally named file instead of prompting for a filename (or erroring). Either change the help to document that --output should be a filename, or explicitly treat output === "file" as “prompt for filename” (or add a separate --output-file).
| if (argv.output) return argv.output; | |
| if (argv.output) { | |
| if (argv.output === "file") { | |
| try { | |
| const fileResponse = await prompter.prompt({ | |
| type: "input", | |
| name: "filename", | |
| message: "Enter output filename:", | |
| initial: "exports.json" | |
| }) as { filename: string }; | |
| return fileResponse.filename; | |
| } catch (error: any) { | |
| if (error.message === 'canceled') { | |
| logger.info("\nOperation cancelled by user."); | |
| process.exit(0); | |
| } | |
| throw error; | |
| } | |
| } | |
| return argv.output; | |
| } |
| @@ -0,0 +1,24 @@ | |||
| export interface ExportInfo { | |||
| name: string; | |||
| kind: "function" | "class" | "type" | "const"; | |||
There was a problem hiding this comment.
ExportInfo.kind doesn’t include categories like interface, enum, or namespace, but the extractor will encounter these in .d.ts files. This forces misclassification (currently defaulting to "const"). Expand the union (or document a deliberate mapping, e.g. interface→"type") to keep the output semantically accurate.
| kind: "function" | "class" | "type" | "const"; | |
| kind: "function" | "class" | "type" | "const" | "interface" | "enum" | "namespace"; |
| // Generate cache key from project path | ||
| const cacheKey = crypto.createHash('md5').update(projectPath).digest('hex'); | ||
|
|
||
| // Check cache first |
There was a problem hiding this comment.
cacheKey is computed from the initial projectPath before you potentially normalize it to the parent directory when tsconfig.json isn’t found. This can cause the same project to be cached under different keys depending on which subdirectory the user points at (and can also cache results under a key that no longer matches the actual analyzed project). Compute the cache key after resolving the final projectPath/tsconfigPath.
src/mcp-ts-introspect/introspect.ts
Outdated
| const uniqueExports = Array.from( | ||
| new Map(allExports.map(exp => [exp.name, exp])).values() |
There was a problem hiding this comment.
Deduping package exports by name alone can drop valid distinct exports (overloads, namespaces merged with functions, re-exports with different type signatures, etc.). Consider using a compound key (e.g., name + kind + typeSignature) similar to the project-mode dedupe logic, or keep an array per name.
| const uniqueExports = Array.from( | |
| new Map(allExports.map(exp => [exp.name, exp])).values() | |
| const buildExportDedupKey = (exp: ExportInfo): string => { | |
| // Use a compound key so that distinct exports with the same name | |
| // (e.g. overloads, merged declarations) are not collapsed. | |
| const e: any = exp; | |
| const kind = e.kind ?? ""; | |
| const typeSig = e.typeSignature ?? e.type ?? ""; | |
| const ns = e.namespace ?? e.moduleName ?? ""; | |
| return `${exp.name}::${kind}::${typeSig}::${ns}`; | |
| }; | |
| const uniqueExports = Array.from( | |
| new Map(allExports.map(exp => [buildExportDedupKey(exp), exp])).values() |
| const options: IntrospectOptions = { | ||
| searchPaths: Array.isArray(argv.searchPaths) ? argv.searchPaths : | ||
| argv.searchPaths ? [argv.searchPaths] : [], | ||
| searchTerm: argv.searchTerm, | ||
| cache: argv.cache, | ||
| cacheDir: argv.cacheDir, | ||
| limit: argv.limit | ||
| }; |
There was a problem hiding this comment.
--search-paths from the help text won’t be picked up here because minimist won’t camelCase --search-paths into searchPaths by default (it will populate argv["search-paths"]). As written, user-supplied search paths will be ignored. Consider normalizing the flag name (e.g., read argv["search-paths"], add an alias, or switch to a parser that supports camelCase).
| @@ -0,0 +1,212 @@ | |||
| import { join, dirname, basename } from "node:path"; | |||
There was a problem hiding this comment.
Unused import basename.
| import { join, dirname, basename } from "node:path"; | |
| import { join, dirname } from "node:path"; |
- Fix critical stale cleanup scope bug (Thread #4/#11): project-scoped listing no longer deletes other projects' cache entries - Preserve matchSnippet when merging search results (Thread #19) - Fix rg-only results getting 0 slots when meta returns 20 (Thread #13) - Use rmSync with force flag instead of repetitive try/catch (Thread #1) - Add try/catch fallback for METADATA_VERSION (Thread #3/#6) - Add -- and -F flags to ripgrep commands (Thread #5/#17) - Reuse extractTextFromMessage in rgExtractSnippet (Thread #2) - Add 10MB file size cap + early exit in extraction (Thread #7/#18) - Add error handling for ripgrep functions (Thread #10/#16)
- Fix critical stale cleanup scope bug (Thread #4/#11): project-scoped listing no longer deletes other projects' cache entries - Preserve matchSnippet when merging search results (Thread #19) - Fix rg-only results getting 0 slots when meta returns 20 (Thread #13) - Use rmSync with force flag instead of repetitive try/catch (Thread #1) - Add try/catch fallback for METADATA_VERSION (Thread #3/#6) - Add -- and -F flags to ripgrep commands (Thread #5/#17) - Reuse extractTextFromMessage in rgExtractSnippet (Thread #2) - Add 10MB file size cap + early exit in extraction (Thread #7/#18) - Add error handling for ripgrep functions (Thread #10/#16)
#25) * fix(claude-history): full metadata extraction, rg search, auto-reindex - Read entire JSONL files for metadata (was limited to 50 lines/64KB) - Store full firstPrompt (was truncated to 120 chars) - Index ALL user messages into allUserText field (capped 5000 chars) - Summary/custom-title: latest wins (captures tail of file) - Add ripgrep full-content search fallback (rgSearchFiles/rgExtractSnippet) - Auto-reindex via MD5 hash of lib.ts+cache.ts (METADATA_VERSION) - Clean up stale cache entries for deleted session files - Rename DB from stats-cache.db to index.db - Show match snippets in claude-resume session picker - Search firstPrompt in matchByIdOrName * feat(claude-resume): show conversation excerpts in session picker Adds a second line below each session name showing additional context: summary (if name is from title), first prompt (if name is from summary), or match snippet (if from search). * fix(claude-resume): run metadata + rg search in parallel, merge results Previously rg only ran as fallback when metadata returned 0 results. Now both phases run concurrently via Promise.all. Results are deduped and merged: metadata matches first (ranked by relevance), then rg-only matches with snippets. Catches content in assistant messages and user text past the 5000-char metadata cap. * fix(claude-history): resolve dashed project names, add nerdy stats - extractProjectName: progressively resolve encoded dir against filesystem to recover dashed names (col-fe was showing as "fe") - Cache resolved project names to avoid repeated filesystem lookups - Show detailed stats in session picker: session count, project count, scope, indexed/stale/reindex status * feat(claude-resume): nerdy search diagnostics Show detailed breakdown of where results came from: - Index matches: count + what was searched (name/branch/project/prompt) - Content search: meta hits, rg total hits, overlap, rg-only unique hits * fix(claude-history): address PR #25 review comments - Fix critical stale cleanup scope bug (Thread #4/#11): project-scoped listing no longer deletes other projects' cache entries - Preserve matchSnippet when merging search results (Thread #19) - Fix rg-only results getting 0 slots when meta returns 20 (Thread #13) - Use rmSync with force flag instead of repetitive try/catch (Thread #1) - Add try/catch fallback for METADATA_VERSION (Thread #3/#6) - Add -- and -F flags to ripgrep commands (Thread #5/#17) - Reuse extractTextFromMessage in rgExtractSnippet (Thread #2) - Add 10MB file size cap + early exit in extraction (Thread #7/#18) - Add error handling for ripgrep functions (Thread #10/#16) * fix(claude-history): add cwd to early-exit check, use null for empty allUserText - Include cwd in early-exit condition to avoid skipping cwd extraction - Return null instead of "" for allUserText when no user text found - Anonymize path examples in comments
- Replace duplicate stripAnsi with import from utils/string (#18) - Fix wrapToWidth ANSI-aware truncation (#1/#3/#22) - Add await to recursive executeTool call (#9/#20) - Guard cursor when filtered list is empty (#5/#19) - Use basename() instead of split("/").pop() (#10/#16) - Validate --width NaN input (#14) - Re-display watch message after screen clear (#15)
- Replace duplicate stripAnsi with import from utils/string (#18) - Fix wrapToWidth ANSI-aware truncation (#1/#3/#22) - Add await to recursive executeTool call (#9/#20) - Guard cursor when filtered list is empty (#5/#19) - Use basename() instead of split("/").pop() (#10/#16) - Validate --width NaN input (#14) - Re-display watch message after screen clear (#15)
* fix: clean error formatting and fuzzy match for tools entry point * feat(tools): add discovery, introspection, and search-select modules - discovery.ts: scans src/ for tools, extracts descriptions from README.md - introspect.ts: runs --help on tools and parses Commander output into structured data - search-select.ts: single-select search prompt with onHighlight callback, modeled on search-multiselect * feat(tools): interactive browser with search, README preview, and subcommand explorer * feat(markdown): add render options and CLI flags (--watch, --width, --theme, --no-color) * fix: improve fuzzy matching to handle trailing dashes and segment matching "tools cli-" now correctly matches "markdown-cli" by stripping trailing dashes and checking dash-separated segments. * feat(markdown-cli): add demo subcommand with interactive template gallery * Revert "feat(markdown-cli): add demo subcommand with interactive template gallery" This reverts commit f5c84db. * feat(markdown): implement theme palette system (dark/light/minimal) Wire up the --theme flag to actual color palettes that change mermaid blocks, table borders, and alert colors per theme. * fix: address PR #28 code review feedback - Replace duplicate stripAnsi with import from utils/string (#18) - Fix wrapToWidth ANSI-aware truncation (#1/#3/#22) - Add await to recursive executeTool call (#9/#20) - Guard cursor when filtered list is empty (#5/#19) - Use basename() instead of split("/").pop() (#10/#16) - Validate --width NaN input (#14) - Re-display watch message after screen clear (#15) * fix(markdown-cli/tools): address PR #28 review feedback - Remove stray n8n.json (unrelated to PR scope) - fix(markdown-cli): reject invalid --theme values via Commander .choices() (Thread #23) - fix(tools): guard discoverTools against missing srcDir with existsSync check (Thread #24) - fix(markdown): use display width for emoji/wide chars in wrapToWidth (Thread #25)
- Use Promise.allSettled in showConfig to handle stale tokens (#14) - Clamp pct in renderBar to prevent RangeError (#17) - Clear firedThresholds on period reset in watch mode (#18) - Wrap entire getKeychainCredentials in try/catch for non-macOS (#19) - Escape sound parameter in AppleScript notifications (#20) - Fix import.meta.dir off-by-one in update tool (#21) - Use isCancelled() in withCancel() for light-mode cancel (#22) - Remove 100% cap on projected usage
- Use Promise.allSettled in showConfig to handle stale tokens (#14) - Clamp pct in renderBar to prevent RangeError (#17) - Clear firedThresholds on period reset in watch mode (#18) - Wrap entire getKeychainCredentials in try/catch for non-macOS (#19) - Escape sound parameter in AppleScript notifications (#20) - Fix import.meta.dir off-by-one in update tool (#21) - Use isCancelled() in withCancel() for light-mode cancel (#22) - Remove 100% cap on projected usage
06d4f64 to
a3be48c
Compare
a3be48c to
74a8971
Compare
There was a problem hiding this comment.
Actionable comments posted: 16
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/mcp-ts-introspect/cache.ts`:
- Around line 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.
In `@src/mcp-ts-introspect/exportExtractor.ts`:
- Around line 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.
- Around line 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.
- Around line 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.
- 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.
In `@src/mcp-ts-introspect/index.ts`:
- Around line 1-4: Replace the Enquirer and minimist imports/usages in
src/mcp-ts-introspect/index.ts with the recommended libraries: import commander
(use the Program API / program.option and program.command to parse arguments and
subcommands instead of minimist) and import `@clack/prompts` (use functions like
text, select, confirm from `@clack/prompts` in place of Enquirer prompts); update
any code that calls Enquirer.prompt(...) and any direct minimist argv parsing to
use commander.command/option and its parsed values, and replace Enquirer prompt
calls with the equivalent `@clack/prompts` functions, leaving clipboardy and chalk
imports as-is.
- Line 1: Replace use of chalk with picocolors across this module: remove the
"import chalk from 'chalk'" and import the functions you need from 'picocolors'
(e.g., import { red, green, yellow, bold } from 'picocolors'), then update all
places that call chalk.* (refer to usages of the imported symbol "chalk") to
call the corresponding picocolors functions (e.g., chalk.red -> red, chalk.bold
-> bold) so terminal coloring uses picocolors APIs instead of chalk.
- Around line 3-4: Replace the two imports of Enquirer and minimist with the
project's approved libraries: use commander for CLI argument parsing and
`@clack/prompts` for interactive prompts; specifically remove references to
Enquirer and minimist in src/mcp-ts-introspect/index.ts, import and use
commander.Command (or the top-level commander API) where minimist was used to
parse argv, and replace Enquirer prompt calls with the appropriate
`@clack/prompts` functions (e.g., select/text/confirm) so the same prompt flow and
option names are preserved; ensure you update any variable names that referenced
Enquirer/minimist (e.g., Enquirer, minimist, parsed args) to the new commander
and `@clack/prompts` identifiers.
In `@src/mcp-ts-introspect/introspect.ts`:
- Around line 88-89: introspectPackage currently deduplicates exports using only
export.name (uniqueExports in introspectPackage) while introspectProject uses a
composite key of name:typeSignature, causing inconsistent behavior for
overloaded or same-named exports; update introspectPackage's deduplication to
use the same composite key as introspectProject (e.g.,
`${exp.name}:${exp.typeSignature}` or equivalent field) when building the Map so
both functions produce consistent uniqueExports results and preserve distinct
overloads or module-specific exports.
- Around line 14-20: The withTimeout function leaves the timeout timer active
even after the input promise settles; modify withTimeout so the setTimeout
returns a timer id (e.g., timerId) and ensure you call clearTimeout(timerId)
when the original promise resolves or rejects (or when the race settles) to
avoid keeping the timer active—update the Promise.race logic in withTimeout to
clear the timeout in both success and failure paths referencing withTimeout and
the timeout timer id.
- Around line 154-165: The code reassigns the function parameter projectPath
when falling back to the parent directory; instead, keep the original parameter
immutable by introducing a new variable (e.g., resolvedProjectPath or
projectRoot) and use that for subsequent path logic: compute tsconfigPath from
resolvedProjectPath, check existsSync, compute parentPath as needed, set
resolvedProjectPath = parentPath when falling back, and leave the original
projectPath untouched; update references in this block (tsconfigPath, parentPath
checks) to use the new variable so behavior is identical but the parameter is
not reassigned.
In `@src/mcp-ts-introspect/mcp-server.ts`:
- Around line 47-138: The three handlers inside server.setRequestHandler
("introspect-package", "introspect-source", "introspect-project") duplicate
identical ZodError catch logic; extract a small helper (e.g.,
parseOrThrow(schema, value) or formatZodError(error) used by
parseWithValidation) that calls schema.parse, catches z.ZodError and throws a
new Error with the formatted message, then replace the direct calls to
IntrospectPackageSchema.parse, IntrospectSourceSchema.parse and
IntrospectProjectSchema.parse with the helper; ensure the helper references
z.ZodError for type checking and is used in each try block so the catch blocks
can be removed or simplified.
In `@src/mcp-ts-introspect/packageResolver.ts`:
- Around line 46-62: The fuzzy match using entry.includes(packageName) can
return wrong packages; change the check in the loop that examines entries to a
precise match: compare entry exactly to packageName or to known pnpm-stored
variants (e.g., entry.startsWith(packageName + "@") and
entry.startsWith(packageName.replace('/', '+') + "@") or
entry.startsWith(packageName.replace('/', '+') + "+") as needed) so only the
intended package (or package@version/store-encoded scoped name) is selected;
update the check around the entry variable (in the loop in packageResolver.ts
where packageName is used) to use these stricter comparisons before constructing
packagePath and packageJsonPath.
In `@src/mcp-ts-introspect/types.ts`:
- Around line 1-6: The ExportInfo.kind union is too narrow and can miscategorize
exports; update the ExportInfo interface (symbol: ExportInfo, property: kind) to
include other possible export kinds such as "interface", "enum", "namespace",
and "variable" (and any other project-specific variants you expect) so the
extractor can represent those exports accurately; adjust any downstream
switch/handling logic that discriminates on ExportInfo.kind to handle the new
values or provide a default case.
In `@src/mcp-ts-introspect/utils.ts`:
- Around line 9-16: The code constructs a RegExp from external searchTerm and
runs regex.test during filtering (see new RegExp(searchTerm, "i"),
regex.test(...) and the filtered = exports.filter(...) usage), which is
vulnerable to ReDoS; validate searchTerm with safe-regex before compiling and
executing: import safe-regex, check safeRegex(searchTerm) (and also enforce a
reasonable max length), and if validation fails either escape the input (use a
safe escaping fallback) or reject/skip regex path and fall back to a simple
substring case-insensitive match for name/typeSignature/description so
regex.test is never called on unsafe patterns. Ensure these checks occur before
new RegExp(...) and before any regex.test(...) calls.
In `@tsconfig.json`:
- Around line 34-36: The tsconfig currently sets "downlevelIteration": true
while the "target" is "ESNext"; remove the "downlevelIteration" setting (or add
a comment) because it has no effect when targeting ESNext—locate the
"downlevelIteration" entry in tsconfig.json and either delete it or replace it
with a short comment explaining why it remains (e.g., kept for older-target
compatibility) so intent is clear.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: cc6862aa-a4ad-479e-bdd7-a496ef72627f
📒 Files selected for processing (10)
src/mcp-ts-introspect/README.mdsrc/mcp-ts-introspect/cache.tssrc/mcp-ts-introspect/exportExtractor.tssrc/mcp-ts-introspect/index.tssrc/mcp-ts-introspect/introspect.tssrc/mcp-ts-introspect/mcp-server.tssrc/mcp-ts-introspect/packageResolver.tssrc/mcp-ts-introspect/types.tssrc/mcp-ts-introspect/utils.tstsconfig.json
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.ts: Usebun runto execute TypeScript files directly without a build step
Place general-purpose helper functions insrc/utils/instead of tool directories
Never add// src/path/to/file.tsas the first line of files
Don't add comments that restate what the code already says (e.g., obvious descriptions of function calls)
Always use block form with braces forifstatements, even for early returns; never use one-lineifstatements
Add an empty line beforeifstatements unless the preceding line is a variable declaration used by thatif
Add an empty line after closing}unless followed byelse,catch,finally, or another}
Noas anytype assertions; use proper type narrowing, type guards, or explicit interfaces instead
Use discriminant checks (e.g.,entity.className === "User") when working with union types instead of type assertions
Prefererror: errovererror: err instanceof Error ? err.message : String(err)when the error field accepts unknown type
Files:
src/mcp-ts-introspect/utils.tssrc/mcp-ts-introspect/cache.tssrc/mcp-ts-introspect/types.tssrc/mcp-ts-introspect/mcp-server.tssrc/mcp-ts-introspect/introspect.tssrc/mcp-ts-introspect/index.tssrc/mcp-ts-introspect/exportExtractor.tssrc/mcp-ts-introspect/packageResolver.ts
src/**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
src/**/*.ts: Use@clack/promptsfor interactive user prompts (preferred over@inquirer/prompts)
UseBun.spawn()for executing external commands and always check exit codes with meaningful error messages
Use Node.jspathmodule for cross-platform path handling and resolve relative paths to absolute usingresolve()
Use Bun's native file APIs (Bun.write()) for better performance instead of other file writing methods
Use centralized logging with pino logger outputting to/logs/directory organized by date
Handle user cancellation from prompts gracefully
Provide sensible defaults and suggestions in user prompts
Strip ANSI codes fromchalkcolored output for non-TTY environments
Files:
src/mcp-ts-introspect/utils.tssrc/mcp-ts-introspect/cache.tssrc/mcp-ts-introspect/types.tssrc/mcp-ts-introspect/mcp-server.tssrc/mcp-ts-introspect/introspect.tssrc/mcp-ts-introspect/index.tssrc/mcp-ts-introspect/exportExtractor.tssrc/mcp-ts-introspect/packageResolver.ts
src/**/index.ts
📄 CodeRabbit inference engine (CLAUDE.md)
src/**/index.ts: Usecommanderfor parsing command-line arguments with subcommands and options
Support multiple output destinations (file, clipboard, stdout) and useclipboardyfor clipboard operations
Tool discovery checks for directories withindex.tsorindex.tsx(tool name = directory name) and standalone.tsor.tsxfiles (tool name = filename without extension)
Respect--silentand--verboseflags in tool output handling
Files:
src/mcp-ts-introspect/index.ts
🧠 Learnings (19)
📓 Common learnings
Learnt from: genesiscz
Repo: genesiscz/GenesisTools PR: 82
File: src/claude/lib/usage/dashboard-config.ts:24-25
Timestamp: 2026-03-09T13:39:43.059Z
Learning: GenesisTools (genesiscz/GenesisTools) is a personal CLI tool, not a multi-tenant service. Do not suggest data migration paths or backward-compatibility shims for simple default value changes in configuration files (e.g., dashboard-config.ts). The TUI provides interactive controls (e.g., the `i` key to cycle poll intervals at runtime), so users can adjust settings without needing automated migrations. Avoid overengineering suggestions in this context.
Learnt from: genesiscz
Repo: genesiscz/GenesisTools PR: 81
File: src/github/commands/get.ts:209-212
Timestamp: 2026-03-09T13:14:03.722Z
Learning: In the GenesisTools repo (genesiscz/GenesisTools), there is no CI formatter (e.g., Prettier/Biome) configured or enforced. Do not flag formatting mismatches based on static analysis tool annotations (e.g., GitHub Actions CI formatter warnings) in this repository, as they are not representative of actual enforced formatting rules. TypeScript files under src/ should be reviewed for logical correctness and consistency with existing code patterns instead.
Learnt from: CR
Repo: genesiscz/GenesisTools PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-20T01:35:08.233Z
Learning: Applies to src/**/index.ts : Tool discovery checks for directories with `index.ts` or `index.tsx` (tool name = directory name) and standalone `.ts` or `.tsx` files (tool name = filename without extension)
📚 Learning: 2026-02-20T00:52:27.023Z
Learnt from: genesiscz
Repo: genesiscz/GenesisTools PR: 31
File: src/ask/utils/helpers.ts:3-3
Timestamp: 2026-02-20T00:52:27.023Z
Learning: In all TypeScript source files under src, prefer using picocolors for colored terminal output in new code. Picocolors is smaller and faster than chalk, so adopt it for CLI output coloring and avoid adding chalk in new code paths unless there is a compelling compatibility reason.
Applied to files:
src/mcp-ts-introspect/utils.tssrc/mcp-ts-introspect/cache.tssrc/mcp-ts-introspect/types.tssrc/mcp-ts-introspect/mcp-server.tssrc/mcp-ts-introspect/introspect.tssrc/mcp-ts-introspect/index.tssrc/mcp-ts-introspect/exportExtractor.tssrc/mcp-ts-introspect/packageResolver.ts
📚 Learning: 2026-02-24T15:32:37.494Z
Learnt from: genesiscz
Repo: genesiscz/GenesisTools PR: 54
File: src/github/lib/output.ts:109-113
Timestamp: 2026-02-24T15:32:37.494Z
Learning: In TypeScript files under src/, do not require a leading blank line before an if statement that is the first statement inside a function body (immediately after the function signature). The blank line rule should only apply to if statements that come after other statements within the function body. Apply this guideline consistently across TS files in src to reduce unnecessary vertical whitespace and keep concise function bodies.
Applied to files:
src/mcp-ts-introspect/utils.tssrc/mcp-ts-introspect/cache.tssrc/mcp-ts-introspect/types.tssrc/mcp-ts-introspect/mcp-server.tssrc/mcp-ts-introspect/introspect.tssrc/mcp-ts-introspect/index.tssrc/mcp-ts-introspect/exportExtractor.tssrc/mcp-ts-introspect/packageResolver.ts
📚 Learning: 2026-03-09T13:13:58.786Z
Learnt from: genesiscz
Repo: genesiscz/GenesisTools PR: 81
File: src/github/commands/get.ts:209-212
Timestamp: 2026-03-09T13:13:58.786Z
Learning: In the GenesisTools repo (genesiscz/GenesisTools), do not treat CI formatter warnings as enforceable formatting rules for TypeScript files under src/. Focus reviews on logical correctness and consistency with existing code patterns. For files under src (e.g., src/github/commands/get.ts), prioritize code structure, readability, naming, correctness, and adherence to project conventions over automated formatting warnings from CI tools.
Applied to files:
src/mcp-ts-introspect/utils.tssrc/mcp-ts-introspect/cache.tssrc/mcp-ts-introspect/types.tssrc/mcp-ts-introspect/mcp-server.tssrc/mcp-ts-introspect/introspect.tssrc/mcp-ts-introspect/index.tssrc/mcp-ts-introspect/exportExtractor.tssrc/mcp-ts-introspect/packageResolver.ts
📚 Learning: 2026-02-24T15:32:44.925Z
Learnt from: genesiscz
Repo: genesiscz/GenesisTools PR: 54
File: src/github/lib/review-output.ts:18-20
Timestamp: 2026-02-24T15:32:44.925Z
Learning: In TypeScript files, do not require a blank line between the opening brace of a function and the first statement if the first statement is the if statement immediately after the signature. The blank-line rule applies to separating an if from unrelated preceding code within the same block, not to spacing after the function opening brace. Apply this rule to all TS functions across the codebase.
Applied to files:
src/mcp-ts-introspect/utils.tssrc/mcp-ts-introspect/cache.tssrc/mcp-ts-introspect/types.tssrc/mcp-ts-introspect/mcp-server.tssrc/mcp-ts-introspect/introspect.tssrc/mcp-ts-introspect/index.tssrc/mcp-ts-introspect/exportExtractor.tssrc/mcp-ts-introspect/packageResolver.ts
📚 Learning: 2026-02-20T01:35:08.233Z
Learnt from: CR
Repo: genesiscz/GenesisTools PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-20T01:35:08.233Z
Learning: Applies to src/**/*.ts : Provide sensible defaults and suggestions in user prompts
Applied to files:
src/mcp-ts-introspect/README.mdsrc/mcp-ts-introspect/index.ts
📚 Learning: 2026-03-09T13:39:43.059Z
Learnt from: genesiscz
Repo: genesiscz/GenesisTools PR: 82
File: src/claude/lib/usage/dashboard-config.ts:24-25
Timestamp: 2026-03-09T13:39:43.059Z
Learning: GenesisTools (genesiscz/GenesisTools) is a personal CLI tool, not a multi-tenant service. Do not suggest data migration paths or backward-compatibility shims for simple default value changes in configuration files (e.g., dashboard-config.ts). The TUI provides interactive controls (e.g., the `i` key to cycle poll intervals at runtime), so users can adjust settings without needing automated migrations. Avoid overengineering suggestions in this context.
Applied to files:
src/mcp-ts-introspect/README.md
📚 Learning: 2026-02-20T00:52:23.917Z
Learnt from: genesiscz
Repo: genesiscz/GenesisTools PR: 31
File: .claude/commands/release.md:50-55
Timestamp: 2026-02-20T00:52:23.917Z
Learning: The GenesisTools repository uses "master" as its default branch, not "main". Commands and scripts targeting the default branch should reference "master" explicitly.
Applied to files:
src/mcp-ts-introspect/README.md
📚 Learning: 2026-02-25T23:00:07.620Z
Learnt from: genesiscz
Repo: genesiscz/GenesisTools PR: 56
File: plugins/genesis-tools/commands/github-pr.md:131-131
Timestamp: 2026-02-25T23:00:07.620Z
Learning: Adopt the style: use lowercase 'markdown' (not 'Markdown') in the GenesisTools documentation. Apply this consistently across all Markdown files in the repository (any .md file), including generated docs and READMEs.
Applied to files:
src/mcp-ts-introspect/README.md
📚 Learning: 2026-02-20T01:35:08.233Z
Learnt from: CR
Repo: genesiscz/GenesisTools PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-20T01:35:08.233Z
Learning: Applies to **/*.ts : Use discriminant checks (e.g., `entity.className === "User"`) when working with union types instead of type assertions
Applied to files:
src/mcp-ts-introspect/types.ts
📚 Learning: 2026-02-20T01:35:08.233Z
Learnt from: CR
Repo: genesiscz/GenesisTools PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-20T01:35:08.233Z
Learning: Applies to **/*.ts : No `as any` type assertions; use proper type narrowing, type guards, or explicit interfaces instead
Applied to files:
src/mcp-ts-introspect/types.ts
📚 Learning: 2026-02-20T01:35:08.233Z
Learnt from: CR
Repo: genesiscz/GenesisTools PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-20T01:35:08.233Z
Learning: Applies to src/**/index.ts : Tool discovery checks for directories with `index.ts` or `index.tsx` (tool name = directory name) and standalone `.ts` or `.tsx` files (tool name = filename without extension)
Applied to files:
src/mcp-ts-introspect/introspect.tssrc/mcp-ts-introspect/index.tssrc/mcp-ts-introspect/packageResolver.ts
📚 Learning: 2026-02-20T01:35:08.233Z
Learnt from: CR
Repo: genesiscz/GenesisTools PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-20T01:35:08.233Z
Learning: Applies to src/**/*.ts : Use `clack/prompts` for interactive user prompts (preferred over `inquirer/prompts`)
Applied to files:
src/mcp-ts-introspect/index.ts
📚 Learning: 2026-02-20T01:35:08.233Z
Learnt from: CR
Repo: genesiscz/GenesisTools PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-20T01:35:08.233Z
Learning: Applies to src/**/index.ts : Use `commander` for parsing command-line arguments with subcommands and options
Applied to files:
src/mcp-ts-introspect/index.ts
📚 Learning: 2026-02-20T01:35:08.233Z
Learnt from: CR
Repo: genesiscz/GenesisTools PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-20T01:35:08.233Z
Learning: Applies to src/**/index.ts : Support multiple output destinations (file, clipboard, stdout) and use `clipboardy` for clipboard operations
Applied to files:
src/mcp-ts-introspect/index.ts
📚 Learning: 2026-02-20T01:35:08.233Z
Learnt from: CR
Repo: genesiscz/GenesisTools PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-20T01:35:08.233Z
Learning: Applies to src/**/index.ts : Respect `--silent` and `--verbose` flags in tool output handling
Applied to files:
src/mcp-ts-introspect/index.ts
📚 Learning: 2026-02-20T01:35:08.233Z
Learnt from: CR
Repo: genesiscz/GenesisTools PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-20T01:35:08.233Z
Learning: The `tools` executable shows an interactive tool selector using inquirer/prompts when run without arguments
Applied to files:
src/mcp-ts-introspect/index.ts
📚 Learning: 2026-02-20T01:35:08.233Z
Learnt from: CR
Repo: genesiscz/GenesisTools PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-20T01:35:08.233Z
Learning: Applies to **/*.ts : Don't add comments that restate what the code already says (e.g., obvious descriptions of function calls)
Applied to files:
src/mcp-ts-introspect/exportExtractor.ts
📚 Learning: 2026-02-20T01:35:08.233Z
Learnt from: CR
Repo: genesiscz/GenesisTools PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-20T01:35:08.233Z
Learning: Applies to src/**/*.ts : Use Node.js `path` module for cross-platform path handling and resolve relative paths to absolute using `resolve()`
Applied to files:
src/mcp-ts-introspect/packageResolver.ts
🧬 Code graph analysis (6)
src/mcp-ts-introspect/utils.ts (1)
src/mcp-ts-introspect/types.ts (1)
ExportInfo(1-6)
src/mcp-ts-introspect/cache.ts (1)
src/mcp-ts-introspect/types.ts (2)
ExportInfo(1-6)CacheEntry(21-24)
src/mcp-ts-introspect/mcp-server.ts (2)
src/mcp-ts-introspect/types.ts (1)
IntrospectOptions(8-14)src/mcp-ts-introspect/introspect.ts (3)
introspectPackage(22-101)introspectSource(103-134)introspectProject(136-211)
src/mcp-ts-introspect/introspect.ts (5)
src/mcp-ts-introspect/types.ts (2)
IntrospectOptions(8-14)ExportInfo(1-6)src/mcp-ts-introspect/cache.ts (2)
loadCache(9-32)saveCache(34-53)src/mcp-ts-introspect/utils.ts (1)
filterExports(4-38)src/mcp-ts-introspect/packageResolver.ts (2)
findPackageJsonAndDir(7-86)findDeclarationFiles(101-155)src/mcp-ts-introspect/exportExtractor.ts (1)
extractExports(5-19)
src/mcp-ts-introspect/index.ts (4)
src/ask/types/config.ts (1)
Args(30-32)src/mcp-ts-introspect/types.ts (2)
ExportInfo(1-6)IntrospectOptions(8-14)src/mcp-ts-introspect/mcp-server.ts (1)
startMcpServer(33-244)src/mcp-ts-introspect/introspect.ts (3)
introspectPackage(22-101)introspectSource(103-134)introspectProject(136-211)
src/mcp-ts-introspect/exportExtractor.ts (1)
src/mcp-ts-introspect/types.ts (1)
ExportInfo(1-6)
🪛 ast-grep (0.41.0)
src/mcp-ts-introspect/utils.ts
[warning] 9-9: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(searchTerm, "i")
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🪛 GitHub Actions: CI
src/mcp-ts-introspect/introspect.ts
[error] 4-4: Cannot find module 'ts-morph' or its corresponding type declarations.
src/mcp-ts-introspect/index.ts
[error] 3-3: Cannot find module 'enquirer' or its corresponding type declarations.
[error] 4-4: Cannot find a declaration file for module 'minimist'. '/home/runner/work/GenesisTools/GenesisTools/node_modules/minimist/index.js' implicitly has an 'any' type. Try npm i --save-dev @types/minimist`` if it exists or add a new declaration (.d.ts) file containing declare module 'minimist';
src/mcp-ts-introspect/exportExtractor.ts
[error] 1-1: Cannot find module 'ts-morph' or its corresponding type declarations.
🔇 Additional comments (12)
src/mcp-ts-introspect/README.md (1)
1-150: LGTM!The README is comprehensive and well-structured, covering CLI usage, all three introspection modes, MCP server configuration, and available tools. The examples are practical and the documentation aligns with the implemented features.
src/mcp-ts-introspect/types.ts (1)
8-24: LGTM!The
IntrospectOptions,PackageLocation, andCacheEntryinterfaces are well-defined with appropriate optional fields and clear semantics.src/mcp-ts-introspect/cache.ts (1)
34-52: LGTM!The
saveCachefunction correctly ensures the directory exists, handles errors gracefully without throwing, and logs outcomes appropriately. UsingBun.writealigns with the coding guidelines.src/mcp-ts-introspect/packageResolver.ts (2)
101-155: LGTM!The
findDeclarationFilesfunction has well-ordered fallback strategies (types/typings field → exports field → index.d.ts → main.d.ts → full scan) and handles errors gracefully.
187-214: LGTM!The
findAllDeclarationFilesfunction correctly implements depth-limited recursive scanning, excludes hidden directories andnode_modules, and handles directory read errors gracefully.src/mcp-ts-introspect/mcp-server.ts (1)
140-237: LGTM!The
ListToolsRequestSchemahandler correctly exposes the available tools with their input schemas. The schemas are well-documented with descriptions and appropriate defaults.src/mcp-ts-introspect/index.ts (3)
238-255: LGTM for argument handling structure.The minimist configuration properly handles aliases, boolean flags, string options, and defaults. The structure is well-organized.
268-324: LGTM for mode execution logic.The switch-case structure for handling different introspection modes is clean, with proper async/await usage and verbose logging support.
329-341: LGTM for output handling.Correctly supports all three output destinations (clipboard, stdout, file) using appropriate libraries (
clipboardy,Bun.write), and provides user feedback for non-stdout outputs.src/mcp-ts-introspect/exportExtractor.ts (1)
82-110: LGTM!The defensive checks for
getJsDocsavailability and the parent fallback for variable declarations are well-designed. Theas unknown aspattern is acceptable here given the runtime check guards it.src/mcp-ts-introspect/introspect.ts (2)
103-134: LGTM!The in-memory project setup for source code introspection is clean and appropriately skips caching since the source is ephemeral.
183-194: LGTM!Good resilience pattern: catching per-file errors and continuing with other files ensures partial results are still returned when individual files fail.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| @@ -0,0 +1,110 @@ | |||
| import { type Symbol as MorphSymbol, Node, type SourceFile } from "ts-morph"; | |||
There was a problem hiding this comment.
🧩 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.
| 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; | ||
| } |
There was a problem hiding this comment.
🧹 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.
| const declaration = declarations[0]; | ||
| const node = declaration as Node; |
There was a problem hiding this comment.
🧹 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.
| 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.
| 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"; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| server.setRequestHandler(CallToolRequestSchema, async (request) => { | ||
| if (request.params.name === "introspect-package") { | ||
| try { | ||
| const args = IntrospectPackageSchema.parse(request.params.arguments); | ||
| const options: IntrospectOptions = { | ||
| searchPaths: args.searchPaths, | ||
| searchTerm: args.searchTerm, | ||
| cache: args.cache, | ||
| cacheDir: args.cacheDir, | ||
| limit: args.limit, | ||
| }; | ||
|
|
||
| const exports = await introspectPackage(args.packageName, options); | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: "text", | ||
| text: JSON.stringify(exports, null, 2), | ||
| }, | ||
| ], | ||
| }; | ||
| } catch (error) { | ||
| if (error instanceof z.ZodError) { | ||
| throw new Error( | ||
| `Invalid arguments: ${error.errors.map((e) => `${e.path}: ${e.message}`).join(", ")}` | ||
| ); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| if (request.params.name === "introspect-source") { | ||
| try { | ||
| const args = IntrospectSourceSchema.parse(request.params.arguments); | ||
| const options: IntrospectOptions = { | ||
| searchTerm: args.searchTerm, | ||
| limit: args.limit, | ||
| }; | ||
|
|
||
| const exports = await introspectSource(args.sourceCode, options); | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: "text", | ||
| text: JSON.stringify(exports, null, 2), | ||
| }, | ||
| ], | ||
| }; | ||
| } catch (error) { | ||
| if (error instanceof z.ZodError) { | ||
| throw new Error( | ||
| `Invalid arguments: ${error.errors.map((e) => `${e.path}: ${e.message}`).join(", ")}` | ||
| ); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| if (request.params.name === "introspect-project") { | ||
| try { | ||
| const args = IntrospectProjectSchema.parse(request.params.arguments); | ||
| const options: IntrospectOptions = { | ||
| searchTerm: args.searchTerm, | ||
| cache: args.cache, | ||
| cacheDir: args.cacheDir, | ||
| limit: args.limit, | ||
| }; | ||
|
|
||
| const exports = await introspectProject(args.projectPath, options); | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: "text", | ||
| text: JSON.stringify(exports, null, 2), | ||
| }, | ||
| ], | ||
| }; | ||
| } catch (error) { | ||
| if (error instanceof z.ZodError) { | ||
| throw new Error( | ||
| `Invalid arguments: ${error.errors.map((e) => `${e.path}: ${e.message}`).join(", ")}` | ||
| ); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| throw new Error(`Unknown tool: ${request.params.name}`); | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider extracting common error handling logic.
The try/catch blocks for all three tool handlers have identical ZodError handling. This could be extracted to reduce duplication.
♻️ Proposed refactor
+function handleToolError(error: unknown): never {
+ if (error instanceof z.ZodError) {
+ throw new Error(
+ `Invalid arguments: ${error.errors.map((e) => `${e.path}: ${e.message}`).join(", ")}`
+ );
+ }
+ throw error;
+}
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "introspect-package") {
try {
// ... existing code ...
} catch (error) {
- if (error instanceof z.ZodError) {
- throw new Error(
- `Invalid arguments: ${error.errors.map((e) => `${e.path}: ${e.message}`).join(", ")}`
- );
- }
- throw error;
+ handleToolError(error);
}
}
// ... similar for other handlers
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/mcp-ts-introspect/mcp-server.ts` around lines 47 - 138, The three
handlers inside server.setRequestHandler ("introspect-package",
"introspect-source", "introspect-project") duplicate identical ZodError catch
logic; extract a small helper (e.g., parseOrThrow(schema, value) or
formatZodError(error) used by parseWithValidation) that calls schema.parse,
catches z.ZodError and throws a new Error with the formatted message, then
replace the direct calls to IntrospectPackageSchema.parse,
IntrospectSourceSchema.parse and IntrospectProjectSchema.parse with the helper;
ensure the helper references z.ZodError for type checking and is used in each
try block so the catch blocks can be removed or simplified.
| const entries = await readdir(pnpmPath); | ||
| for (const entry of entries) { | ||
| if (entry.includes(packageName)) { | ||
| const packagePath = join(pnpmPath, entry, "node_modules", packageName); | ||
| const packageJsonPath = join(packagePath, "package.json"); | ||
|
|
||
| if (existsSync(packageJsonPath)) { | ||
| // Resolve symlinks for pnpm | ||
| const realPackagePath = realpathSync(packagePath); | ||
| logger.info(`Found package in pnpm at: ${realPackagePath}`); | ||
| return { | ||
| packageJsonPath: join(realPackagePath, "package.json"), | ||
| packageDir: realPackagePath, | ||
| }; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Fuzzy pnpm directory matching may return wrong package.
The entry.includes(packageName) check on line 48 could match unintended packages. For example, searching for typescript would also match typescript-eslint, typescript-transform, etc.
Consider using a more precise match pattern.
🔧 Proposed fix for precise matching
for (const entry of entries) {
- if (entry.includes(packageName)) {
+ // pnpm directory format: package-name@version or `@scope`+package-name@version
+ const escapedName = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const pnpmPattern = new RegExp(`^(@[^+]+\\+)?${escapedName.replace('/', '\\+')}@`);
+ if (pnpmPattern.test(entry)) {
const packagePath = join(pnpmPath, entry, "node_modules", packageName);📝 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.
| const entries = await readdir(pnpmPath); | |
| for (const entry of entries) { | |
| if (entry.includes(packageName)) { | |
| const packagePath = join(pnpmPath, entry, "node_modules", packageName); | |
| const packageJsonPath = join(packagePath, "package.json"); | |
| if (existsSync(packageJsonPath)) { | |
| // Resolve symlinks for pnpm | |
| const realPackagePath = realpathSync(packagePath); | |
| logger.info(`Found package in pnpm at: ${realPackagePath}`); | |
| return { | |
| packageJsonPath: join(realPackagePath, "package.json"), | |
| packageDir: realPackagePath, | |
| }; | |
| } | |
| } | |
| } | |
| const entries = await readdir(pnpmPath); | |
| for (const entry of entries) { | |
| // pnpm directory format: package-name@version or `@scope`+package-name@version | |
| const escapedName = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| const pnpmPattern = new RegExp(`^(@[^+]+\\+)?${escapedName.replace('/', '\\+')}@`); | |
| if (pnpmPattern.test(entry)) { | |
| const packagePath = join(pnpmPath, entry, "node_modules", packageName); | |
| const packageJsonPath = join(packagePath, "package.json"); | |
| if (existsSync(packageJsonPath)) { | |
| // Resolve symlinks for pnpm | |
| const realPackagePath = realpathSync(packagePath); | |
| logger.info(`Found package in pnpm at: ${realPackagePath}`); | |
| return { | |
| packageJsonPath: join(realPackagePath, "package.json"), | |
| packageDir: realPackagePath, | |
| }; | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/mcp-ts-introspect/packageResolver.ts` around lines 46 - 62, The fuzzy
match using entry.includes(packageName) can return wrong packages; change the
check in the loop that examines entries to a precise match: compare entry
exactly to packageName or to known pnpm-stored variants (e.g.,
entry.startsWith(packageName + "@") and
entry.startsWith(packageName.replace('/', '+') + "@") or
entry.startsWith(packageName.replace('/', '+') + "+") as needed) so only the
intended package (or package@version/store-encoded scoped name) is selected;
update the check around the entry variable (in the loop in packageResolver.ts
where packageName is used) to use these stricter comparisons before constructing
packagePath and packageJsonPath.
| export interface ExportInfo { | ||
| name: string; | ||
| kind: "function" | "class" | "type" | "const"; | ||
| typeSignature: string; | ||
| description: string | null; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider expanding the kind union type.
The current kind values cover common cases, but TypeScript exports can also include "interface", "enum", "namespace", and "variable". If the export extractor encounters these, they may be miscategorized.
💡 Potential expansion
export interface ExportInfo {
name: string;
- kind: "function" | "class" | "type" | "const";
+ kind: "function" | "class" | "type" | "const" | "interface" | "enum" | "namespace" | "variable";
typeSignature: string;
description: string | null;
}📝 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.
| export interface ExportInfo { | |
| name: string; | |
| kind: "function" | "class" | "type" | "const"; | |
| typeSignature: string; | |
| description: string | null; | |
| } | |
| export interface ExportInfo { | |
| name: string; | |
| kind: "function" | "class" | "type" | "const" | "interface" | "enum" | "namespace" | "variable"; | |
| typeSignature: string; | |
| description: string | null; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/mcp-ts-introspect/types.ts` around lines 1 - 6, The ExportInfo.kind union
is too narrow and can miscategorize exports; update the ExportInfo interface
(symbol: ExportInfo, property: kind) to include other possible export kinds such
as "interface", "enum", "namespace", and "variable" (and any other
project-specific variants you expect) so the extractor can represent those
exports accurately; adjust any downstream switch/handling logic that
discriminates on ExportInfo.kind to handle the new values or provide a default
case.
| try { | ||
| const regex = new RegExp(searchTerm, "i"); | ||
| filtered = exports.filter( | ||
| (exp) => | ||
| regex.test(exp.name) || | ||
| regex.test(exp.typeSignature) || | ||
| (exp.description && regex.test(exp.description)) | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What is the safe-regex npm package and how to use it for ReDoS prevention?
💡 Result:
safe-regex is an npm package that heuristically flags regular expressions that might exhibit catastrophic backtracking (ReDoS). It does this by analyzing the pattern (not running it) and, per its README, “limits the star height to 1”; it returns a simple boolean “safe/unsafe” result. It also warns there can be false positives and false negatives. [1]
Install
npm i safe-regex[1]
Use in code (boolean “is this pattern safe?”)
const safe = require('safe-regex');
// string pattern
console.log(safe('(x+x+)+y')); // false (likely vulnerable)
console.log(safe('(beep|boop)*')); // true
// RegExp object also works
console.log(safe(/\blocation\s*:[^:\n]+\b(Oakland|San Francisco)\b/)); // trueThese examples mirror the package’s README. [1]
Tuning strictness with opts.limit
safe(re, opts) accepts:
opts.limit: “maximum number of allowed repetitions in the entire regex” (default25). [1]
const ok = safe('(a+){10}', { limit: 5 }); // stricter => more likely to return false[1]
How this helps with ReDoS prevention (practical pattern)
Use safe-regex as a gate before compiling/using regexes that can be influenced by untrusted input (user-provided patterns, or developer patterns applied to untrusted text):
function compileUserRegex(pattern, flags='') {
if (!safe(pattern)) throw new Error('Rejected potentially unsafe regex');
return new RegExp(pattern, flags);
}[1]
Important limitations / stronger defenses
- Because
safe-regexcan miss issues, consider additional defenses like input-length limits and/or switching to a non-backtracking engine such as RE2 (vianode-re2), which is commonly used specifically to mitigate ReDoS risk. [1][2]
Sources: safe-regex README on npm (description, API, examples, opts.limit, limitations) [1]; RE2/node-re2 rationale for ReDoS protection [2].
🏁 Script executed:
# Find and read the utils.ts file
fd -e ts -p "*utils.ts" | head -20Repository: genesiscz/GenesisTools
Length of output: 354
🏁 Script executed:
# Check package.json for current dependencies
cat package.json | head -50Repository: genesiscz/GenesisTools
Length of output: 2081
🏁 Script executed:
# Find utils.ts with proper glob syntax
fd --glob "**/utils.ts" 2>/dev/null | grep -i "mcp-ts-introspect"Repository: genesiscz/GenesisTools
Length of output: 96
🏁 Script executed:
# Read full package.json and check for safe-regex
cat package.jsonRepository: genesiscz/GenesisTools
Length of output: 5137
🏁 Script executed:
cat -n src/mcp-ts-introspect/utils.tsRepository: genesiscz/GenesisTools
Length of output: 1772
🏁 Script executed:
# Search for filterExports usage to understand MCP exposure
rg "filterExports" src/mcp-ts-introspect/ -A 2 -B 2Repository: genesiscz/GenesisTools
Length of output: 2616
🏁 Script executed:
# Check the main MCP server implementation
fd --glob "**/mcp-ts-introspect/*.ts" | head -20Repository: genesiscz/GenesisTools
Length of output: 48
🏁 Script executed:
# Find MCP server implementation
fd --glob "**/mcp-ts-introspect/*.ts" -x wc -lRepository: genesiscz/GenesisTools
Length of output: 48
🏁 Script executed:
# Look for MCP tools/handlers to see if searchTerm is exposed externally
rg "searchTerm" src/mcp-ts-introspect/ -B 5 -A 2Repository: genesiscz/GenesisTools
Length of output: 16324
ReDoS vulnerability with externally-provided regex patterns in MCP server.
The MCP server explicitly exposes searchTerm as user input across three tools (introspect-package, introspect-source, introspect-project), where it is used directly in new RegExp(searchTerm, "i"). The current try-catch fallback only catches syntax errors during regex construction—it does not protect against Regular Expression Denial of Service (ReDoS) attacks, which occur during pattern matching (the .test() calls) with valid but pathological patterns.
Add regex validation before compilation using safe-regex:
+import safeRegex from "safe-regex";
+
export function filterExports(exports: ExportInfo[], searchTerm?: string, limit?: number): ExportInfo[] {
let filtered = exports;
// Apply search filter
if (searchTerm) {
try {
+ // Validate pattern to prevent catastrophic backtracking
+ if (!safeRegex(searchTerm)) {
+ logger.warn(`Potentially unsafe regex pattern detected, falling back to string match`);
+ throw new Error("Unsafe regex");
+ }
const regex = new RegExp(searchTerm, "i");Note: safe-regex uses heuristics and can have false positives/negatives. Consider additional defenses like input length limits or a timeout wrapper around regex operations if further hardening is needed.
🧰 Tools
🪛 ast-grep (0.41.0)
[warning] 9-9: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(searchTerm, "i")
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/mcp-ts-introspect/utils.ts` around lines 9 - 16, The code constructs a
RegExp from external searchTerm and runs regex.test during filtering (see new
RegExp(searchTerm, "i"), regex.test(...) and the filtered = exports.filter(...)
usage), which is vulnerable to ReDoS; validate searchTerm with safe-regex before
compiling and executing: import safe-regex, check safeRegex(searchTerm) (and
also enforce a reasonable max length), and if validation fails either escape the
input (use a safe escaping fallback) or reject/skip regex path and fall back to
a simple substring case-insensitive match for name/typeSignature/description so
regex.test is never called on unsafe patterns. Ensure these checks occur before
new RegExp(...) and before any regex.test(...) calls.
| // Required for compatibility | ||
| "esModuleInterop": true, | ||
| "downlevelIteration": true, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
downlevelIteration may be unnecessary with ESNext target.
The downlevelIteration option is used when targeting older JavaScript versions (ES5/ES3) to support accurate iteration semantics. Since the project targets ESNext (line 12), this option has no effect—the compiler won't downlevel iteration constructs.
Consider removing it unless you plan to change the target, or clarify the specific compatibility need in the comment.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tsconfig.json` around lines 34 - 36, The tsconfig currently sets
"downlevelIteration": true while the "target" is "ESNext"; remove the
"downlevelIteration" setting (or add a comment) because it has no effect when
targeting ESNext—locate the "downlevelIteration" entry in tsconfig.json and
either delete it or replace it with a short comment explaining why it remains
(e.g., kept for older-target compatibility) so intent is clear.
Summary
mcp-ts-introspecttool for introspecting TypeScript packages (exports, types, structure)Test plan
tools mcp-ts-introspect --helpworksSummary by CodeRabbit
Release Notes
New Features
Documentation
Chores