Skip to main content
Glama
local_fetch_content.ts9.92 kB
import { readFile, stat } from 'fs/promises'; import { resolve, isAbsolute } from 'path'; import { minifyContent } from '../utils/minifier.js'; import { getToolHints } from './hints.js'; import { applyPagination, generatePaginationHints, createPaginationInfo, } from '../utils/pagination.js'; import { RESOURCE_LIMITS, DEFAULTS } from '../constants.js'; import { validateToolPath, createErrorResult, } from '../utils/toolHelpers.js'; import type { FetchContentQuery, FetchContentResult } from '../types.js'; import { ToolErrors, ERROR_CODES } from '../errors/errorCodes.js'; export async function fetchContent( query: FetchContentQuery ): Promise<FetchContentResult> { try { const pathValidation = validateToolPath(query, 'LOCAL_FETCH_CONTENT'); if (!pathValidation.isValid) { return pathValidation.errorResult as FetchContentResult; } const absolutePath = isAbsolute(query.path) ? query.path : resolve(process.cwd(), query.path); let fileStats; try { fileStats = await stat(absolutePath); } catch (error) { const toolError = ToolErrors.fileAccessFailed( query.path, error instanceof Error ? error : undefined ); return createErrorResult( toolError, 'LOCAL_FETCH_CONTENT', query, { path: query.path } ) as FetchContentResult; } const fileSizeKB = fileStats.size / 1024; if ( fileSizeKB > RESOURCE_LIMITS.LARGE_FILE_THRESHOLD_KB && !query.charLength && !query.matchString ) { const toolError = ToolErrors.fileTooLarge( query.path, fileSizeKB, RESOURCE_LIMITS.LARGE_FILE_THRESHOLD_KB ); return createErrorResult( toolError, 'LOCAL_FETCH_CONTENT', query, { path: query.path, hints: [ 'Best approach: Use matchString to extract specific functions/classes you actually need', 'Alternative: Use charLength for pagination if you need to browse through the file systematically', 'Why matchString works better: Gets only relevant sections, faster, and uses fewer tokens', 'Critical: fullContent without charLength will fail on large files - always specify a reading strategy', ], } ) as FetchContentResult; } let content: string; try { content = await readFile(absolutePath, 'utf-8'); } catch (error) { const toolError = ToolErrors.fileReadFailed( query.path, error instanceof Error ? error : undefined ); return createErrorResult( toolError, 'LOCAL_FETCH_CONTENT', query, { path: query.path } ) as FetchContentResult; } const lines = content.split('\n'); const totalLines = lines.length; let resultContent: string; let isPartial = false; if (query.matchString) { const result = extractMatchingLines( lines, query.matchString, query.matchStringContextLines ?? 5, query.matchStringIsRegex ?? false, query.matchStringCaseSensitive ?? false ); if (result.lines.length === 0) { const contextHints = [ `Searched ${totalLines} line${totalLines === 1 ? '' : 's'} - no matches found`, ]; // Add pattern-specific hints if (query.matchStringIsRegex) { contextHints.push( 'TIP: Regex pattern may be too specific - try simplifying' ); } else { contextHints.push( 'TIP: Try matchStringIsRegex=true for pattern matching (e.g., "export.*function")' ); } if (query.matchStringCaseSensitive) { contextHints.push( 'TIP: Case-sensitive mode active - try matchStringCaseSensitive=false' ); } contextHints.push( 'TIP: Verify file contains expected content or try simpler pattern' ); return { status: 'empty', path: query.path, errorCode: ERROR_CODES.NO_MATCHES, totalLines, researchGoal: query.researchGoal, reasoning: query.reasoning, hints: [ ...getToolHints('LOCAL_FETCH_CONTENT', 'empty'), '', ...contextHints, ], }; } resultContent = result.lines.join('\n'); if ( !query.charLength && resultContent.length > DEFAULTS.MAX_OUTPUT_CHARS ) { const toolError = ToolErrors.patternTooBroad( query.matchString || '', result.matchRanges.length ); return createErrorResult( toolError, 'LOCAL_FETCH_CONTENT', query, { path: query.path, totalLines, hints: [ `Your pattern matched extensively - likely hitting common code`, 'Make the pattern more specific to target only what you need, or use charLength to paginate results', 'Possible causes: Minified files, repeated patterns, or overly broad match terms', 'Strategy: Refine matchString to be more selective, or explore with pagination', ], } ) as FetchContentResult; } isPartial = true; } else { resultContent = content; isPartial = false; } if (query.minified !== false) { try { const originalLength = resultContent.length; const minifiedContent = minifyContent(resultContent, query.path); if (minifiedContent.length < originalLength) { resultContent = minifiedContent; } } catch { // Keep original if minification fails } } if (!resultContent || resultContent.trim().length === 0) { return { status: 'empty', path: query.path, totalLines, researchGoal: query.researchGoal, reasoning: query.reasoning, hints: getToolHints('LOCAL_FETCH_CONTENT', 'empty'), }; } if (!query.charLength && resultContent.length > DEFAULTS.MAX_OUTPUT_CHARS) { const toolError = ToolErrors.paginationRequired(resultContent.length); return createErrorResult( toolError, 'LOCAL_FETCH_CONTENT', query, { path: query.path, totalLines, hints: [ `RECOMMENDED: charLength=${RESOURCE_LIMITS.RECOMMENDED_CHAR_LENGTH} (paginate results)`, 'ALTERNATIVE: Use matchString for targeted extraction', `NOTE: Results >10K chars require pagination for safety`, ], } ) as FetchContentResult; } const pagination = applyPagination( resultContent, query.charOffset ?? 0, query.charLength ); const baseHints = getToolHints('LOCAL_FETCH_CONTENT', 'hasResults'); const paginationHints = query.charLength ? generatePaginationHints(pagination, { toolName: 'local_fetch_content' }) : []; return { status: 'hasResults', path: query.path, content: pagination.paginatedContent, contentLength: pagination.paginatedContent.length, isPartial, totalLines, ...(query.charLength && { pagination: createPaginationInfo(pagination), }), researchGoal: query.researchGoal, reasoning: query.reasoning, hints: [...baseHints, ...paginationHints], }; } catch (error) { return createErrorResult( error, 'LOCAL_FETCH_CONTENT', query, { path: query.path } ) as FetchContentResult; } } function extractMatchingLines( lines: string[], pattern: string, contextLines: number, isRegex: boolean = false, caseSensitive: boolean = false ): { lines: string[]; matchRanges: Array<{ start: number; end: number }> } { const matchingLineNumbers: number[] = []; // Compile regex once if needed let regex: RegExp | null = null; if (isRegex) { try { const flags = caseSensitive ? '' : 'i'; regex = new RegExp(pattern, flags); } catch (error) { throw new Error( `Invalid regex pattern: ${error instanceof Error ? error.message : String(error)}` ); } } const literalPattern = caseSensitive ? pattern : pattern.toLowerCase(); lines.forEach((line, index) => { const matches = isRegex ? regex!.test(line) : caseSensitive ? line.includes(pattern) : line.toLowerCase().includes(literalPattern); if (matches) { matchingLineNumbers.push(index + 1); } }); if (matchingLineNumbers.length === 0) { return { lines: [], matchRanges: [] }; } // Group consecutive matches to avoid duplicating context const ranges: Array<{ start: number; end: number }> = []; let currentRange = { start: Math.max(1, matchingLineNumbers[0] - contextLines), end: Math.min(lines.length, matchingLineNumbers[0] + contextLines), }; for (let i = 1; i < matchingLineNumbers.length; i++) { const matchLine = matchingLineNumbers[i]; const rangeStart = Math.max(1, matchLine - contextLines); const rangeEnd = Math.min(lines.length, matchLine + contextLines); if (rangeStart <= currentRange.end + 1) { currentRange.end = Math.max(currentRange.end, rangeEnd); } else { ranges.push({ ...currentRange }); currentRange = { start: rangeStart, end: rangeEnd }; } } ranges.push(currentRange); const resultLines: string[] = []; ranges.forEach((range, idx) => { if (idx > 0) { const omittedLines = range.start - ranges[idx - 1].end - 1; if (omittedLines > 0) { resultLines.push(''); resultLines.push(`... [${omittedLines} lines omitted] ...`); resultLines.push(''); } } resultLines.push(...lines.slice(range.start - 1, range.end)); }); return { lines: resultLines, matchRanges: ranges }; }

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/bgauryy/local-explorer-mcp'

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