Skip to main content
Glama
blakeyoder

TypeScript Definitions MCP Server

by blakeyoder
type-indexer.ts13.6 kB
import * as ts from "typescript"; import * as fs from "fs"; import * as path from "path"; import { glob } from "glob"; import { TypeDefinition, PropertyDefinition, MethodDefinition, ParameterDefinition, ProjectConfig, PackageInfo } from "./types.js"; export class TypeIndexer { private program: ts.Program | null = null; private typeChecker: ts.TypeChecker | null = null; private sourceFiles: Map<string, ts.SourceFile> = new Map(); private typeCache: Map<string, TypeDefinition[]> = new Map(); private config: ProjectConfig; constructor(config: ProjectConfig) { this.config = config; } async initialize(): Promise<void> { const compilerOptions = this.getCompilerOptions(); const rootFiles = await this.findTypeScriptFiles(); this.program = ts.createProgram(rootFiles, compilerOptions); this.typeChecker = this.program.getTypeChecker(); // Cache all source files for (const sourceFile of this.program.getSourceFiles()) { if (!sourceFile.isDeclarationFile && rootFiles.includes(sourceFile.fileName)) { this.sourceFiles.set(sourceFile.fileName, sourceFile); } } } private getCompilerOptions(): ts.CompilerOptions { if (this.config.tsconfigPath && fs.existsSync(this.config.tsconfigPath)) { const configFile = ts.readConfigFile(this.config.tsconfigPath, ts.sys.readFile); if (configFile.config) { const parsed = ts.parseJsonConfigFileContent( configFile.config, ts.sys, path.dirname(this.config.tsconfigPath) ); return parsed.options; } } return { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS, lib: ["ES2020", "DOM"], declaration: true, esModuleInterop: true, skipLibCheck: true, strict: true, moduleResolution: ts.ModuleResolutionKind.NodeJs, allowSyntheticDefaultImports: true, resolveJsonModule: true }; } private async findTypeScriptFiles(): Promise<string[]> { const patterns = [ path.join(this.config.rootPath, "**/*.ts"), path.join(this.config.rootPath, "**/*.tsx"), path.join(this.config.rootPath, "**/*.d.ts") ]; if (this.config.includeNodeModules) { patterns.push( path.join(this.config.rootPath, "node_modules/**/*.d.ts"), path.join(this.config.rootPath, "node_modules/@types/**/*.d.ts") ); } const allFiles: string[] = []; for (const pattern of patterns) { const files = await glob(pattern, { ignore: this.config.excludePatterns, absolute: true }); allFiles.push(...files); } return [...new Set(allFiles)]; // Remove duplicates } async findType(typeName: string, packageName?: string): Promise<TypeDefinition[]> { if (!this.program || !this.typeChecker) { throw new Error("TypeIndexer not initialized. Call initialize() first."); } const cacheKey = `${typeName}:${packageName || ""}`; if (this.typeCache.has(cacheKey)) { const cached = this.typeCache.get(cacheKey)!; // Check cache age const now = Date.now(); if (now - this.getCacheTimestamp(cacheKey) < this.config.maxCacheAge) { return cached; } } const results: TypeDefinition[] = []; for (const sourceFile of this.program.getSourceFiles()) { if (packageName && !this.isFromPackage(sourceFile.fileName, packageName)) { continue; } const definitions = this.extractTypeDefinitions(sourceFile, typeName); results.push(...definitions); } this.typeCache.set(cacheKey, results); this.setCacheTimestamp(cacheKey, Date.now()); return results; } private isFromPackage(fileName: string, packageName: string): boolean { return fileName.includes(`node_modules/${packageName}`) || fileName.includes(`node_modules/@types/${packageName.replace("@", "").replace("/", "__")}`); } private extractTypeDefinitions(sourceFile: ts.SourceFile, typeName?: string): TypeDefinition[] { const definitions: TypeDefinition[] = []; const visit = (node: ts.Node) => { if (typeName && !this.nodeMatchesTypeName(node, typeName)) { ts.forEachChild(node, visit); return; } const definition = this.processNodeByKind(node, sourceFile); if (definition) { if (Array.isArray(definition)) { definitions.push(...definition); } else { definitions.push(definition); } } ts.forEachChild(node, visit); }; visit(sourceFile); return definitions; } private nodeMatchesTypeName(node: ts.Node, typeName: string): boolean { if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isClassDeclaration(node) || ts.isEnumDeclaration(node)) { return node.name?.getText() === typeName; } if (ts.isFunctionDeclaration(node)) { return node.name?.getText() === typeName; } return true; // For other nodes, let the specific processors decide } // Process nodes by kind using a cleaner pattern private processNodeByKind(node: ts.Node, sourceFile: ts.SourceFile): TypeDefinition | TypeDefinition[] | null { if (ts.isInterfaceDeclaration(node)) { return this.processInterface(node, sourceFile); } if (ts.isTypeAliasDeclaration(node)) { return this.processTypeAlias(node, sourceFile); } if (ts.isClassDeclaration(node)) { return this.processClass(node, sourceFile); } if (ts.isEnumDeclaration(node)) { return this.processEnum(node, sourceFile); } if (ts.isFunctionDeclaration(node)) { return this.processFunction(node, sourceFile); } if (ts.isVariableStatement(node)) { return this.processVariableStatement(node, sourceFile); } return null; } private processInterface(node: ts.InterfaceDeclaration, sourceFile: ts.SourceFile): TypeDefinition { const name = node.name.getText(); const properties: PropertyDefinition[] = []; for (const member of node.members) { if (ts.isPropertySignature(member)) { properties.push(this.processPropertySignature(member)); } } return { name, kind: "interface", file: sourceFile.fileName, line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1, definition: node.getText(), properties, extends: node.heritageClauses?.find(h => h.token === ts.SyntaxKind.ExtendsKeyword) ?.types.map(t => t.getText()) || [], generics: node.typeParameters?.map(t => t.getText()) || [], jsDoc: this.getJsDoc(node), packageName: this.getPackageName(sourceFile.fileName) }; } private processTypeAlias(node: ts.TypeAliasDeclaration, sourceFile: ts.SourceFile): TypeDefinition { return { name: node.name.getText(), kind: "type", file: sourceFile.fileName, line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1, definition: node.getText(), generics: node.typeParameters?.map(t => t.getText()) || [], jsDoc: this.getJsDoc(node), packageName: this.getPackageName(sourceFile.fileName) }; } private processClass(node: ts.ClassDeclaration, sourceFile: ts.SourceFile): TypeDefinition { const name = node.name?.getText() || "anonymous"; const properties: PropertyDefinition[] = []; const methods: MethodDefinition[] = []; for (const member of node.members) { if (ts.isPropertyDeclaration(member)) { properties.push(this.processPropertyDeclaration(member)); } else if (ts.isMethodDeclaration(member)) { methods.push(this.processMethodDeclaration(member)); } } return { name, kind: "class", file: sourceFile.fileName, line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1, definition: node.getText(), properties, methods, extends: node.heritageClauses?.find(h => h.token === ts.SyntaxKind.ExtendsKeyword) ?.types.map(t => t.getText()) || [], implements: node.heritageClauses?.find(h => h.token === ts.SyntaxKind.ImplementsKeyword) ?.types.map(t => t.getText()) || [], generics: node.typeParameters?.map(t => t.getText()) || [], jsDoc: this.getJsDoc(node), packageName: this.getPackageName(sourceFile.fileName) }; } private processEnum(node: ts.EnumDeclaration, sourceFile: ts.SourceFile): TypeDefinition { return { name: node.name.getText(), kind: "enum", file: sourceFile.fileName, line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1, definition: node.getText(), jsDoc: this.getJsDoc(node), packageName: this.getPackageName(sourceFile.fileName) }; } private processFunction(node: ts.FunctionDeclaration, sourceFile: ts.SourceFile): TypeDefinition { const name = node.name?.getText() || "anonymous"; return { name, kind: "function", file: sourceFile.fileName, line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1, definition: node.getText(), jsDoc: this.getJsDoc(node), packageName: this.getPackageName(sourceFile.fileName) }; } private processVariableStatement(node: ts.VariableStatement, sourceFile: ts.SourceFile): TypeDefinition[] { const definitions: TypeDefinition[] = []; for (const declaration of node.declarationList.declarations) { if (declaration.name && ts.isIdentifier(declaration.name)) { definitions.push({ name: declaration.name.getText(), kind: "variable", file: sourceFile.fileName, line: sourceFile.getLineAndCharacterOfPosition(declaration.getStart()).line + 1, definition: node.getText(), jsDoc: this.getJsDoc(node), packageName: this.getPackageName(sourceFile.fileName) }); } } return definitions; } private processPropertySignature(member: ts.PropertySignature): PropertyDefinition { const name = member.name?.getText() || ""; const type = member.type?.getText() || "any"; const optional = !!member.questionToken; const readonly = member.modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) || false; return { name, type, optional, readonly, jsDoc: this.getJsDoc(member) }; } private processPropertyDeclaration(member: ts.PropertyDeclaration): PropertyDefinition { const name = member.name?.getText() || ""; const type = member.type?.getText() || "any"; const optional = !!member.questionToken; const readonly = member.modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) || false; return { name, type, optional, readonly, jsDoc: this.getJsDoc(member) }; } private processMethodDeclaration(member: ts.MethodDeclaration): MethodDefinition { const name = member.name?.getText() || ""; const optional = !!member.questionToken; const returnType = member.type?.getText() || "void"; const parameters: ParameterDefinition[] = []; for (const param of member.parameters) { if (ts.isParameter(param) && param.name && ts.isIdentifier(param.name)) { parameters.push({ name: param.name.getText(), type: param.type?.getText() || "any", optional: !!param.questionToken, defaultValue: param.initializer?.getText() }); } } return { name, parameters, returnType, optional, jsDoc: this.getJsDoc(member) }; } private getJsDoc(node: ts.Node): string | undefined { const jsDoc = ts.getJSDocCommentsAndTags(node); if (jsDoc.length > 0) { return jsDoc.map(j => j.getText()).join("\n"); } return undefined; } private getPackageName(fileName: string): string | undefined { const nodeModulesMatch = fileName.match(/node_modules\/(@?[^\/]+(?:\/[^\/]+)?)/); if (nodeModulesMatch) { return nodeModulesMatch[1]; } return undefined; } // Cache timestamp storage private cacheTimestamps = new Map<string, number>(); private getCacheTimestamp(key: string): number { return this.cacheTimestamps.get(key) ?? 0; } private setCacheTimestamp(key: string, timestamp: number): void { this.cacheTimestamps.set(key, timestamp); } async findInterfaces(pattern: string): Promise<TypeDefinition[]> { if (!this.program) { throw new Error("TypeIndexer not initialized. Call initialize() first."); } const results: TypeDefinition[] = []; const regex = new RegExp(pattern.replace(/\*/g, ".*")); for (const sourceFile of this.program.getSourceFiles()) { const definitions = this.extractTypeDefinitions(sourceFile); const matching = definitions.filter(def => def.kind === "interface" && regex.test(def.name) ); results.push(...matching); } return results; } async getPackageTypes(packageName: string): Promise<TypeDefinition[]> { if (!this.program) { throw new Error("TypeIndexer not initialized. Call initialize() first."); } const results: TypeDefinition[] = []; for (const sourceFile of this.program.getSourceFiles()) { if (this.isFromPackage(sourceFile.fileName, packageName)) { const definitions = this.extractTypeDefinitions(sourceFile); results.push(...definitions); } } return results; } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/blakeyoder/typescript-definitions-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server