Skip to main content
Glama

NervusDB MCP Server

Official
by nervusdb
documentationGenerator.ts25.4 kB
import type { QueryService } from '../domain/query/queryService.js'; import type { DefinitionResult } from './definitionLocator.js'; /** * Documentation format types */ export type DocFormat = 'jsdoc' | 'tsdoc' | 'python-docstring' | 'markdown'; /** * Documentation completeness status */ export type DocStatus = 'complete' | 'partial' | 'missing'; /** * Generated documentation for a symbol */ export interface GeneratedDoc { symbolName: string; filePath: string; format: DocFormat; status: DocStatus; existingDoc?: string; generatedDoc: string; confidence: number; // 0-1 reasoning: string; sections: { summary?: string; description?: string; parameters?: Array<{ name: string; type?: string; description: string; }>; returns?: { type?: string; description: string; }; throws?: Array<{ type: string; description: string; }>; examples?: string[]; seeAlso?: string[]; }; } /** * Documentation analysis result */ export interface DocAnalysisResult { projectPath: string; targetSymbols: string[]; analysis: Array<{ symbolName: string; filePath: string; status: DocStatus; issues: string[]; suggestions: string[]; }>; summary: { total: number; complete: number; partial: number; missing: number; completeness: number; // percentage }; stats: { analysisTimeMs: number; }; } /** * Documentation generation result */ export interface GenerateDocsResult { projectPath: string; generated: GeneratedDoc[]; summary: { total: number; successful: number; failed: number; avgConfidence: number; }; stats: { generationTimeMs: number; }; } /** * Documentation generator service dependencies */ export interface DocumentationGeneratorDeps { queryService: QueryService; } /** * Documentation generator service * * Analyzes code and generates missing documentation with contextual information */ export class DocumentationGenerator { constructor(private deps: DocumentationGeneratorDeps) {} /** * Analyze documentation completeness */ async analyzeDocumentation(params: { projectPath: string; symbols: string[]; }): Promise<DocAnalysisResult> { const startTime = Date.now(); const { projectPath, symbols } = params; const analysis: DocAnalysisResult['analysis'] = []; for (const symbolName of symbols) { try { // Locate symbol definition const entity = await this.deps.queryService.findSymbolDefinition(projectPath, symbolName); const definitions = entity ? [this.toDefinitionResult(entity)] : []; if (definitions.length === 0) { analysis.push({ symbolName, filePath: 'unknown', status: 'missing', issues: ['Symbol not found in project'], suggestions: ['Verify symbol name and project path'], }); continue; } const def = definitions[0]; // Check existing documentation const existingDoc = await this.extractExistingDoc(def); const status = this.assessDocStatus(existingDoc); const issues = this.identifyDocIssues(existingDoc, def); const suggestions = this.generateSuggestions(status, issues); analysis.push({ symbolName, filePath: def.filePath, status, issues, suggestions, }); } catch (error) { analysis.push({ symbolName, filePath: 'unknown', status: 'missing', issues: [`Analysis failed: ${error instanceof Error ? error.message : String(error)}`], suggestions: ['Check if symbol is accessible'], }); } } const summary = this.computeSummary(analysis); return { projectPath, targetSymbols: symbols, analysis, summary, stats: { analysisTimeMs: Date.now() - startTime, }, }; } /** * Generate documentation for symbols */ async generateDocumentation(params: { projectPath: string; symbols: string[]; format?: DocFormat; includeExamples?: boolean; }): Promise<GenerateDocsResult> { const startTime = Date.now(); const { projectPath, symbols, format = 'jsdoc', includeExamples = true } = params; const generated: GeneratedDoc[] = []; for (const symbolName of symbols) { try { const doc = await this.generateDocForSymbol({ symbolName, projectPath, format, includeExamples, }); generated.push(doc); } catch (error) { // Failed generation - add placeholder generated.push({ symbolName, filePath: 'unknown', format, status: 'missing', generatedDoc: `Failed to generate: ${error instanceof Error ? error.message : String(error)}`, confidence: 0, reasoning: 'Generation failed', sections: {}, }); } } const summary = { total: generated.length, successful: generated.filter((d) => d.confidence > 0.5).length, failed: generated.filter((d) => d.confidence <= 0.5).length, avgConfidence: generated.reduce((sum, d) => sum + d.confidence, 0) / (generated.length || 1), }; return { projectPath, generated, summary, stats: { generationTimeMs: Date.now() - startTime, }, }; } /** * Generate documentation for a single symbol */ private async generateDocForSymbol(params: { symbolName: string; projectPath: string; format: DocFormat; includeExamples: boolean; }): Promise<GeneratedDoc> { const { symbolName, projectPath, format, includeExamples } = params; // 1. Locate definition const entity = await this.deps.queryService.findSymbolDefinition(projectPath, symbolName); if (!entity) { throw new Error(`Symbol '${symbolName}' not found`); } const def = this.toDefinitionResult(entity); // 2. Extract existing doc const existingDoc = await this.extractExistingDoc(def); const status = this.assessDocStatus(existingDoc); // 3. Analyze symbol signature and context const signature = this.parseSignature(def); const context = await this.gatherContext(symbolName, def); // 4. Generate sections const sections = await this.generateSections({ def, signature, context, includeExamples, }); // 5. Format documentation const generatedDoc = this.formatDocumentation(sections, format); // 6. Calculate confidence const confidence = this.calculateConfidence({ status, signature, context, sections, }); return { symbolName, filePath: def.filePath, format, status, existingDoc, generatedDoc, confidence, reasoning: this.explainGeneration(signature, context, sections), sections, }; } /** * Convert CodeEntityInfo to DefinitionResult */ private toDefinitionResult( entity: import('../domain/query/queryService.js').CodeEntityInfo, ): DefinitionResult { return { ...entity, confidence: 1.0, matchReason: 'Exact match', }; } /** * Extract existing documentation from definition */ private async extractExistingDoc(_: DefinitionResult): Promise<string | undefined> { // Placeholder: in real implementation, read file and parse doc comment // For now, return undefined (missing) return undefined; } /** * Assess documentation status */ private assessDocStatus(existingDoc?: string): DocStatus { if (!existingDoc || existingDoc.trim().length === 0) { return 'missing'; } // Simple heuristic: check for key sections const hasDescription = existingDoc.length > 50; const hasParams = /@param|:param/.test(existingDoc); const hasReturns = /@returns?|:returns?/.test(existingDoc); if (hasDescription && (hasParams || hasReturns)) { return 'complete'; } else if (hasDescription || hasParams || hasReturns) { return 'partial'; } return 'missing'; } /** * Identify documentation issues */ private identifyDocIssues(existingDoc: string | undefined, def: DefinitionResult): string[] { const issues: string[] = []; if (!existingDoc) { issues.push('No documentation found'); return issues; } // Check for missing sections if (existingDoc.length < 30) { issues.push('Description too brief'); } if (def.signature && def.signature.includes('(') && !/@param/.test(existingDoc)) { issues.push('Missing parameter documentation'); } if (def.type === 'function' && !/@returns?/.test(existingDoc)) { issues.push('Missing return value documentation'); } return issues; } /** * Generate improvement suggestions */ private generateSuggestions(status: DocStatus, issues: string[]): string[] { const suggestions: string[] = []; if (status === 'missing') { suggestions.push('Add complete documentation with summary, params, and returns'); return suggestions; } if (issues.includes('Description too brief')) { suggestions.push('Expand description to explain purpose and behavior'); } if (issues.includes('Missing parameter documentation')) { suggestions.push('Document all parameters with types and descriptions'); } if (issues.includes('Missing return value documentation')) { suggestions.push('Document return value type and meaning'); } suggestions.push('Add usage examples for public API'); suggestions.push('Link to related functions'); return suggestions; } /** * Compute summary statistics */ private computeSummary(analysis: DocAnalysisResult['analysis']): DocAnalysisResult['summary'] { const total = analysis.length; const complete = analysis.filter((a) => a.status === 'complete').length; const partial = analysis.filter((a) => a.status === 'partial').length; const missing = analysis.filter((a) => a.status === 'missing').length; const completeness = total > 0 ? (complete / total) * 100 : 0; return { total, complete, partial, missing, completeness, }; } /** * Parse function signature */ private parseSignature(def: DefinitionResult): { parameters: Array<{ name: string; type?: string }>; returnType?: string; } { const parameters: Array<{ name: string; type?: string }> = []; let returnType: string | undefined; // Simple regex parsing (in real implementation, use AST) if (def.signature) { // Extract parameters: function(param1: type1, param2: type2) const paramsMatch = def.signature.match(/\(([^)]*)\)/); if (paramsMatch) { const paramsStr = paramsMatch[1]; const paramParts = paramsStr .split(',') .map((p) => p.trim()) .filter(Boolean); for (const part of paramParts) { const colonIdx = part.indexOf(':'); if (colonIdx !== -1) { parameters.push({ name: part.substring(0, colonIdx).trim(), type: part.substring(colonIdx + 1).trim(), }); } else { parameters.push({ name: part }); } } } // Extract return type: ): ReturnType const returnMatch = def.signature.match(/\):\s*([^\s{]+)/); if (returnMatch) { returnType = returnMatch[1]; } } return { parameters, returnType }; } /** * Gather contextual information */ private async gatherContext( symbolName: string, def: DefinitionResult, ): Promise<{ usageCount: number; relatedSymbols: string[]; isPublic: boolean; }> { // Placeholder: In real implementation, use ReferencesFinder service // For now, use heuristics based on definition const isPublic = def.signature?.includes('export') || def.signature?.includes('public') || false; return { usageCount: 0, // Unknown without references relatedSymbols: [], isPublic, }; } /** * Generate documentation sections */ private async generateSections(params: { def: DefinitionResult; signature: ReturnType<typeof this.parseSignature>; context: Awaited<ReturnType<typeof this.gatherContext>>; includeExamples: boolean; }): Promise<GeneratedDoc['sections']> { const { def, signature, context, includeExamples } = params; // Generate summary const summary = this.generateSummary(def, context); // Generate description const description = this.generateDescription(def, context); // Generate parameter docs const parameters = signature.parameters.map((param) => ({ name: param.name, type: param.type, description: this.generateParamDescription(param.name, def), })); // Generate return docs const returns = signature.returnType ? { type: signature.returnType, description: this.generateReturnDescription(signature.returnType, def), } : undefined; // Generate examples const examples = includeExamples ? await this.generateExamples(def, context) : undefined; // Generate see also links const seeAlso = context.relatedSymbols.length > 0 ? context.relatedSymbols.slice(0, 3) : undefined; return { summary, description, parameters: parameters.length > 0 ? parameters : undefined, returns, examples, seeAlso, }; } /** * Generate summary line */ private generateSummary( def: DefinitionResult, _: { isPublic: boolean; usageCount: number }, ): string { const name = def.name; const kind = def.type; // Context-aware summary generation if (name.startsWith('get') || name.startsWith('fetch') || name.startsWith('load')) { return `Retrieves ${this.humanizeName(name.replace(/^(get|fetch|load)/, ''))}`; } else if (name.startsWith('set') || name.startsWith('update') || name.startsWith('save')) { return `Updates ${this.humanizeName(name.replace(/^(set|update|save)/, ''))}`; } else if (name.startsWith('delete') || name.startsWith('remove')) { return `Deletes ${this.humanizeName(name.replace(/^(delete|remove)/, ''))}`; } else if (name.startsWith('create') || name.startsWith('build')) { return `Creates ${this.humanizeName(name.replace(/^(create|build)/, ''))}`; } else if ( name.startsWith('validate') || name.startsWith('check') || name.startsWith('verify') ) { return `Validates ${this.humanizeName(name.replace(/^(validate|check|verify)/, ''))}`; } else if (name.startsWith('calculate') || name.startsWith('compute')) { return `Calculates ${this.humanizeName(name.replace(/^(calculate|compute)/, ''))}`; } else if (name.startsWith('format') || name.startsWith('render')) { return `Formats ${this.humanizeName(name.replace(/^(format|render)/, ''))}`; } // Default summary return `${kind}: ${name}`; } /** * Generate detailed description */ private generateDescription( def: DefinitionResult, context: { isPublic: boolean; usageCount: number }, ): string { const lines: string[] = []; lines.push( `This ${def.type} is ${context.isPublic ? 'part of the public API' : 'for internal use'}.`, ); if (context.usageCount > 0) { lines.push(`Currently used in ${context.usageCount} place(s) across the project.`); } else { lines.push('Currently not used in the project (may be a new implementation).'); } return lines.join(' '); } /** * Generate parameter description */ private generateParamDescription(paramName: string, _: DefinitionResult): string { // Pattern-based description generation if (paramName === 'options' || paramName === 'config') { return 'Configuration options'; } else if (paramName.includes('callback') || paramName.includes('handler')) { return 'Callback function to execute'; } else if (paramName.endsWith('Path') || paramName.includes('file')) { return 'File path'; } else if (paramName.endsWith('Id') || paramName === 'id') { return 'Unique identifier'; } else if (paramName.includes('name')) { return 'Name'; } return `The ${paramName} parameter`; } /** * Generate return value description */ private generateReturnDescription(returnType: string, _: DefinitionResult): string { if (returnType.includes('Promise')) { return `A promise that resolves to ${returnType.replace(/Promise<(.*)>/, '$1')}`; } else if (returnType === 'void') { return 'No return value'; } else if (returnType === 'boolean') { return 'True if successful, false otherwise'; } return `Returns ${returnType}`; } /** * Generate usage examples */ private async generateExamples( def: DefinitionResult, context: { usageCount: number }, ): Promise<string[]> { const examples: string[] = []; // Generate basic example const signature = this.parseSignature(def); const exampleCall = this.generateExampleCall(def.name, signature); examples.push(`\`\`\`typescript\n${exampleCall}\n\`\`\``); // If used in project, suggest looking at real usage if (context.usageCount > 0) { examples.push('See usage examples in the project with `code.findReferences`'); } return examples; } /** * Generate example function call */ private generateExampleCall( name: string, signature: ReturnType<typeof this.parseSignature>, ): string { const params = signature.parameters .map((p) => { if (p.type?.includes('string')) return `"example"`; if (p.type?.includes('number')) return `42`; if (p.type?.includes('boolean')) return `true`; if (p.type?.includes('[]')) return `[]`; if (p.type?.includes('{}') || p.type?.includes('object')) return `{}`; return `value`; }) .join(', '); const call = `${name}(${params})`; if (signature.returnType?.includes('Promise')) { return `const result = await ${call};`; } else if (signature.returnType && signature.returnType !== 'void') { return `const result = ${call};`; } return `${call};`; } /** * Format documentation in specified format */ private formatDocumentation(sections: GeneratedDoc['sections'], format: DocFormat): string { switch (format) { case 'jsdoc': case 'tsdoc': return this.formatJSDoc(sections); case 'python-docstring': return this.formatPythonDocstring(sections); case 'markdown': return this.formatMarkdown(sections); default: return this.formatJSDoc(sections); } } /** * Format as JSDoc/TSDoc */ private formatJSDoc(sections: GeneratedDoc['sections']): string { const lines: string[] = ['/**']; if (sections.summary) { lines.push(` * ${sections.summary}`); if (sections.description) { lines.push(' *'); lines.push(` * ${sections.description}`); } } if (sections.parameters && sections.parameters.length > 0) { lines.push(' *'); for (const param of sections.parameters) { const type = param.type ? `{${param.type}} ` : ''; lines.push(` * @param ${type}${param.name} - ${param.description}`); } } if (sections.returns) { lines.push(' *'); const type = sections.returns.type ? `{${sections.returns.type}} ` : ''; lines.push(` * @returns ${type}${sections.returns.description}`); } if (sections.throws && sections.throws.length > 0) { lines.push(' *'); for (const throwsItem of sections.throws) { lines.push(` * @throws {${throwsItem.type}} ${throwsItem.description}`); } } if (sections.examples && sections.examples.length > 0) { lines.push(' *'); lines.push(' * @example'); for (const example of sections.examples) { lines.push(` * ${example.replace(/\n/g, '\n * ')}`); } } if (sections.seeAlso && sections.seeAlso.length > 0) { lines.push(' *'); for (const link of sections.seeAlso) { lines.push(` * @see ${link}`); } } lines.push(' */'); return lines.join('\n'); } /** * Format as Python docstring */ private formatPythonDocstring(sections: GeneratedDoc['sections']): string { const lines: string[] = ['"""']; if (sections.summary) { lines.push(sections.summary); if (sections.description) { lines.push(''); lines.push(sections.description); } } if (sections.parameters && sections.parameters.length > 0) { lines.push(''); lines.push('Args:'); for (const param of sections.parameters) { const type = param.type ? ` (${param.type})` : ''; lines.push(` ${param.name}${type}: ${param.description}`); } } if (sections.returns) { lines.push(''); lines.push('Returns:'); const type = sections.returns.type ? ` (${sections.returns.type})` : ''; lines.push(` ${type}${sections.returns.description}`); } if (sections.throws && sections.throws.length > 0) { lines.push(''); lines.push('Raises:'); for (const throwsItem of sections.throws) { lines.push(` ${throwsItem.type}: ${throwsItem.description}`); } } if (sections.examples && sections.examples.length > 0) { lines.push(''); lines.push('Example:'); for (const example of sections.examples) { lines.push(` ${example.replace(/\n/g, '\n ')}`); } } lines.push('"""'); return lines.join('\n'); } /** * Format as Markdown */ private formatMarkdown(sections: GeneratedDoc['sections']): string { const lines: string[] = []; if (sections.summary) { lines.push(`## ${sections.summary}`); lines.push(''); if (sections.description) { lines.push(sections.description); lines.push(''); } } if (sections.parameters && sections.parameters.length > 0) { lines.push('### Parameters'); lines.push(''); for (const param of sections.parameters) { const type = param.type ? ` \`${param.type}\`` : ''; lines.push(`- **${param.name}**${type}: ${param.description}`); } lines.push(''); } if (sections.returns) { lines.push('### Returns'); lines.push(''); const type = sections.returns.type ? ` \`${sections.returns.type}\`` : ''; lines.push(`${type}${sections.returns.description}`); lines.push(''); } if (sections.examples && sections.examples.length > 0) { lines.push('### Examples'); lines.push(''); for (const example of sections.examples) { lines.push(example); lines.push(''); } } return lines.join('\n'); } /** * Calculate confidence score */ private calculateConfidence(params: { status: DocStatus; signature: ReturnType<typeof this.parseSignature>; context: Awaited<ReturnType<typeof this.gatherContext>>; sections: GeneratedDoc['sections']; }): number { const { status, signature, context, sections } = params; let confidence = 0.5; // Base confidence // Boost for having signature info if (signature.parameters.length > 0 || signature.returnType) { confidence += 0.1; } // Boost for usage context if (context.usageCount > 0) { confidence += Math.min(context.usageCount / 10, 0.2); } // Boost for complete sections if (sections.summary) confidence += 0.05; if (sections.description) confidence += 0.05; if (sections.parameters && sections.parameters.length > 0) confidence += 0.05; if (sections.returns) confidence += 0.05; // Penalize if already complete if (status === 'complete') { confidence *= 0.7; // Lower confidence since doc already exists } return Math.min(confidence, 1.0); } /** * Explain generation reasoning */ private explainGeneration( signature: ReturnType<typeof this.parseSignature>, context: Awaited<ReturnType<typeof this.gatherContext>>, sections: GeneratedDoc['sections'], ): string { const reasons: string[] = []; if (signature.parameters.length > 0) { reasons.push(`Analyzed ${signature.parameters.length} parameter(s)`); } if (signature.returnType) { reasons.push(`Return type: ${signature.returnType}`); } if (context.usageCount > 0) { reasons.push(`Found ${context.usageCount} usage(s) in project`); } if (context.isPublic) { reasons.push('Marked as public API'); } if (sections.examples && sections.examples.length > 0) { reasons.push('Generated usage examples'); } return reasons.join('. '); } /** * Humanize camelCase or snake_case name */ private humanizeName(name: string): string { return name .replace(/([A-Z])/g, ' $1') .replace(/_/g, ' ') .toLowerCase() .trim(); } }

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/nervusdb/nervusdb-mcp'

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