Skip to main content
Glama

AutoDev Codebase MCP Server

by anrgct
qdrant-client.ts9.68 kB
import { QdrantClient, Schemas } from "@qdrant/js-client-rest" import { createHash } from "crypto" import * as path from "path" import { getWorkspacePath } from "../../utils/path" import { IVectorStore, SearchFilter } from "../interfaces/vector-store" import { Payload, VectorStoreSearchResult } from "../interfaces" import { MAX_SEARCH_RESULTS, SEARCH_MIN_SCORE, MAX_LIST_FILES_LIMIT } from "../constants" import { match } from "assert" /** * Qdrant implementation of the vector store interface */ export class QdrantVectorStore implements IVectorStore { private readonly QDRANT_URL = "http://localhost:6333" private readonly vectorSize!: number private readonly DISTANCE_METRIC = "Cosine" private client: QdrantClient private readonly collectionName: string /** * Creates a new Qdrant vector store * @param workspacePath Path to the workspace * @param url Optional URL to the Qdrant server */ constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string) { this.client = new QdrantClient({ url: url ?? this.QDRANT_URL, apiKey, headers: { "User-Agent": "Roo-Code", }, }) // Generate collection name from workspace path const hash = createHash("sha256").update(workspacePath).digest("hex") this.vectorSize = vectorSize this.collectionName = `ws-${hash.substring(0, 16)}` } private async getCollectionInfo(): Promise<Schemas["CollectionInfo"] | null> { try { const collectionInfo = await this.client.getCollection(this.collectionName) return collectionInfo } catch (error: unknown) { if (error instanceof Error) { console.warn( `[QdrantVectorStore] Warning during getCollectionInfo for "${this.collectionName}". Collection may not exist or another error occurred:`, error.message, ) } return null } } /** * Initializes the vector store * @returns Promise resolving to boolean indicating if a new collection was created */ async initialize(): Promise<boolean> { let created = false try { const collectionInfo = await this.getCollectionInfo() if (collectionInfo === null) { // Collection info not retrieved (assume not found or inaccessible), create it await this.client.createCollection(this.collectionName, { vectors: { size: this.vectorSize, distance: this.DISTANCE_METRIC, }, }) created = true } else { // Collection exists, check vector size const existingVectorSize = collectionInfo.config?.params?.vectors?.size if (existingVectorSize === this.vectorSize) { created = false // Exists and correct } else { // Exists but wrong vector size, recreate console.warn( `[QdrantVectorStore] Collection ${this.collectionName} exists with vector size ${existingVectorSize}, but expected ${this.vectorSize}. Recreating collection.`, ) await this.client.deleteCollection(this.collectionName) // Known to exist await this.client.createCollection(this.collectionName, { vectors: { size: this.vectorSize, distance: this.DISTANCE_METRIC, }, }) created = true } } // Create index for filePath to support range queries for directoryPrefix filtering try { await this.client.createPayloadIndex(this.collectionName, { field_name: "filePath", field_schema: "keyword", }) } catch (indexError: any) { const errorMessage = (indexError?.message || "").toLowerCase() if (!errorMessage.includes("already exists")) { console.warn( `[QdrantVectorStore] Could not create payload index for filePath on ${this.collectionName}. Details:`, indexError?.message || indexError, ) } } return created } catch (error: any) { console.error( `[QdrantVectorStore] Failed to initialize Qdrant collection "${this.collectionName}":`, error?.message || error, ) throw error } } /** * Upserts points into the vector store * @param points Array of points to upsert */ async upsertPoints( points: Array<{ id: string vector: number[] payload: Record<string, any> }>, ): Promise<void> { try { const processedPoints = points.map((point) => { if (point.payload?.['filePath']) { const segments = point.payload['filePath'].split(path.sep).filter(Boolean) const pathSegments = segments.reduce( (acc: Record<string, string>, segment: string, index: number) => { acc[index.toString()] = segment return acc }, {}, ) return { ...point, payload: { ...point.payload, pathSegments, }, } } return point }) await this.client.upsert(this.collectionName, { points: processedPoints, wait: true, }) } catch (error) { console.error("Failed to upsert points:", error) throw error } } /** * Checks if a payload is valid * @param payload Payload to check * @returns Boolean indicating if the payload is valid */ private isPayloadValid(payload: Record<string, unknown> | null | undefined): payload is Payload { if (!payload) { return false } const validKeys = ["filePath", "codeChunk", "startLine", "endLine"] const hasValidKeys = validKeys.every((key) => key in payload) return hasValidKeys } /** * Searches for similar vectors * @param queryVector Vector to search for * @param filter Search filter options * @returns Promise resolving to search results */ async search( queryVector: number[], filter?: SearchFilter, ): Promise<VectorStoreSearchResult[]> { try { const { pathFilters, minScore, limit = MAX_SEARCH_RESULTS } = filter || {} let qdrantFilter: any = undefined // Build filter based on pathFilters if (pathFilters && pathFilters.length > 0) { // Use pathFilters - treat all as text patterns that can match any part of file paths const shouldConditions = pathFilters.map(pattern => ({ key: "filePath", match: { text: pattern.replace(/\\/g, '/'), // Normalize path separators } })) qdrantFilter = { should: shouldConditions, } } const searchRequest = { query: queryVector, filter: qdrantFilter, score_threshold: minScore ?? SEARCH_MIN_SCORE, limit: limit, params: { hnsw_ef: 128, exact: false, }, with_payload: true, } console.log("🔍[QdrantVectorStore] Search request:", JSON.stringify({...searchRequest, query:"[query vector]"})) const operationResult = await this.client.query(this.collectionName, searchRequest) const filteredPoints = operationResult.points.filter((p) => this.isPayloadValid(p.payload)) return filteredPoints.map(point => ({ id: point.id, score: point.score, payload: point.payload as Payload })) as VectorStoreSearchResult[] } catch (error) { console.error("Failed to search points:", error) throw error } } /** * Deletes points by file path * @param filePath Path of the file to delete points for */ async deletePointsByFilePath(filePath: string): Promise<void> { return this.deletePointsByMultipleFilePaths([filePath]) } async deletePointsByMultipleFilePaths(filePaths: string[]): Promise<void> { if (filePaths.length === 0) { return } try { const filter = { should: filePaths.map((filePath) => ({ key: "filePath", match: { value: filePath, }, })), } await this.client.delete(this.collectionName, { filter, wait: true, }) } catch (error) { console.error("Failed to delete points by file paths:", error) throw error } } /** * Deletes the entire collection. */ async deleteCollection(): Promise<void> { try { // Check if collection exists before attempting deletion to avoid errors if (await this.collectionExists()) { await this.client.deleteCollection(this.collectionName) } } catch (error) { console.error(`[QdrantVectorStore] Failed to delete collection ${this.collectionName}:`, error) throw error // Re-throw to allow calling code to handle it } } /** * Clears all points from the collection */ async clearCollection(): Promise<void> { try { await this.client.delete(this.collectionName, { filter: { must: [], }, wait: true, }) } catch (error) { console.error("Failed to clear collection:", error) throw error } } /** * Checks if the collection exists * @returns Promise resolving to boolean indicating if the collection exists */ async collectionExists(): Promise<boolean> { const collectionInfo = await this.getCollectionInfo() return collectionInfo !== null } async getAllFilePaths(): Promise<string[]> { try { const allFilePaths = new Set<string>() let nextPageOffset: Schemas["ExtendedPointId"] | undefined = undefined do { const response: Schemas["ScrollResult"] = await this.client.scroll(this.collectionName, { limit: 250, with_payload: ["filePath"], with_vector: false, offset: nextPageOffset, }) for (const point of response.points) { if (point.payload?.['filePath'] && typeof point.payload['filePath'] === 'string') { allFilePaths.add(point.payload['filePath']) } } nextPageOffset = response.next_page_offset as Schemas["ExtendedPointId"] | undefined } while (nextPageOffset) return Array.from(allFilePaths) } catch (error) { // console.error("[QdrantVectorStore] Failed to get all file paths:", error) // In case of an error (e.g., collection not found), return an empty array // This prevents the reconciliation process from accidentally deleting everything // if Qdrant is temporarily unavailable. return [] } } }

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/anrgct/autodev-codebase'

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