Skip to main content
Glama

greptile-mcp

greptile.ts9.3 kB
import type { Repository, QueryMessage, QueryResponse, StreamingChunk, GreptileError, Config, } from '../types/index.js'; import { retry, safeJsonParse } from '../utils/index.js'; interface FetchRequestOptions { method: string; headers: Record<string, string>; signal: AbortSignal; body?: string; } export class GreptileClient { private readonly apiKey: string; private readonly githubToken: string; private readonly baseUrl: string; private readonly defaultTimeout: number; private readonly headers: Record<string, string>; constructor(config: Config) { // Allow empty keys for testing, but warn about limited functionality if (!config.apiKey) { console.warn('⚠️ No Greptile API key provided - functionality will be limited'); } if (!config.githubToken) { console.warn('⚠️ No GitHub token provided - repository access will be limited'); } this.apiKey = config.apiKey || ''; this.githubToken = config.githubToken || ''; this.baseUrl = config.baseUrl || 'https://api.greptile.com/v2'; this.defaultTimeout = 60000; // 60 seconds this.headers = { Authorization: `Bearer ${this.apiKey}`, 'X-GitHub-Token': this.githubToken, 'Content-Type': 'application/json', 'User-Agent': 'greptile-mcp-server/3.0.0', }; } /** * Index a repository for code search and querying */ async indexRepository( remote: string, repository: string, branch: string, reload: boolean = true, notify: boolean = false, timeout?: number ): Promise<Record<string, unknown>> { const url = `${this.baseUrl}/repositories`; const payload = { remote, repository, branch, reload, notify, }; return this.makeRequest('POST', url, payload, timeout); } /** * Query repositories to get an answer with code references */ async queryRepositories( messages: QueryMessage[], repositories: Repository[], sessionId?: string, stream: boolean = false, genius: boolean = true, timeout?: number ): Promise<QueryResponse | AsyncIterable<StreamingChunk>> { if (stream) { return this.streamQueryRepositories(messages, repositories, sessionId, genius, timeout); } const url = `${this.baseUrl}/query`; const payload: Record<string, unknown> = { messages, stream: false, genius, }; if (repositories.length > 0) { payload.repositories = repositories; } if (sessionId) { payload.sessionId = sessionId; } const response = await this.makeRequest('POST', url, payload, timeout); return response as QueryResponse; } /** * Stream query repositories with Server-Sent Events */ async *streamQueryRepositories( messages: QueryMessage[], repositories: Repository[], sessionId?: string, genius: boolean = true, timeout?: number ): AsyncIterable<StreamingChunk> { const url = `${this.baseUrl}/query`; const payload: Record<string, unknown> = { messages, stream: true, genius, }; if (repositories.length > 0) { payload.repositories = repositories; } if (sessionId) { payload.sessionId = sessionId; } const streamHeaders = { ...this.headers, Accept: 'text/event-stream', 'Cache-Control': 'no-cache', }; const controller = new AbortController(); const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : null; try { const response = await fetch(url, { method: 'POST', headers: streamHeaders, body: JSON.stringify(payload), signal: controller.signal, }); if (!response.ok) { throw this.createError(`HTTP ${response.status}: ${response.statusText}`, response.status); } if (!response.body) { throw this.createError('No response body for streaming request'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.trim() && line.startsWith('data: ')) { const data = line.slice(6); const chunk = safeJsonParse(data, null); if (chunk) { const processedChunk = this.processStreamChunk(chunk); if (processedChunk) { yield processedChunk; } } } } } } finally { reader.releaseLock(); } } finally { if (timeoutId) { clearTimeout(timeoutId); } } } /** * Get information about an indexed repository */ async getRepositoryInfo( remote: string, repository: string, branch: string, timeout?: number ): Promise<Record<string, unknown>> { const repositoryId = `${remote}:${branch}:${repository}`; const encodedId = encodeURIComponent(repositoryId); const url = `${this.baseUrl}/repositories/${encodedId}`; return this.makeRequest('GET', url, undefined, timeout); } /** * Make an HTTP request with retry logic and error handling */ private async makeRequest( method: string, url: string, payload?: unknown, timeout?: number ): Promise<Record<string, unknown>> { // Check if API key is available for actual requests if (!this.apiKey) { throw new Error( 'Greptile API key is required for API calls. Please configure your API key in the session settings.' ); } const requestTimeout = timeout || this.defaultTimeout; const makeRequestAttempt = async (): Promise<Record<string, unknown>> => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), requestTimeout); try { const requestOptions: FetchRequestOptions = { method, headers: this.headers, signal: controller.signal, }; if (payload) { requestOptions.body = JSON.stringify(payload); } const response = await fetch(url, requestOptions); if (!response.ok) { const errorText = await response.text(); throw this.createError( `HTTP ${response.status}: ${response.statusText}\n${errorText}`, response.status, errorText ); } const result = await response.json(); return result as Record<string, unknown>; } finally { clearTimeout(timeoutId); } }; try { return await retry(makeRequestAttempt, { maxAttempts: 3, baseDelay: 1000, maxDelay: 5000, }); } catch (error) { if (error instanceof Error) { throw this.createError(error.message); } throw this.createError('Unknown error occurred'); } } /** * Process streaming chunks into standardized format */ private processStreamChunk(chunk: unknown): StreamingChunk | null { if (!chunk || typeof chunk !== 'object') { return null; } const chunkObj = chunk as Record<string, unknown>; const timestamp = Date.now(); if (chunkObj.type === 'text' && typeof chunkObj.content === 'string') { return { type: 'text', content: chunkObj.content, timestamp, }; } if (chunkObj.type === 'citation') { return { type: 'citation', file: typeof chunkObj.file === 'string' ? chunkObj.file : undefined, lines: typeof chunkObj.lines === 'string' ? chunkObj.lines : undefined, timestamp, }; } if ('sessionId' in chunkObj && typeof chunkObj.sessionId === 'string') { return { type: 'session', sessionId: chunkObj.sessionId, timestamp, }; } return { type: 'other', data: chunk, timestamp, }; } /** * Create a standardized error object */ private createError(message: string, statusCode?: number, response?: unknown): GreptileError { const error = new Error(message) as GreptileError; error.name = 'GreptileError'; if (statusCode) { error.statusCode = statusCode; } if (response) { error.response = response; } return error; } /** * Health check for the Greptile API */ async healthCheck(): Promise<boolean> { try { // Create manual timeout controller for better compatibility const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); const response = await fetch(`${this.baseUrl}/health`, { method: 'GET', headers: this.headers, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { return false; } const text = await response.text(); return text.includes('Healthy'); } catch (error) { console.error('Health check failed:', error); return false; } } }

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/sosacrazy126/greptile-mcp'

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