RagDocs MCP Server
- src
import { QdrantClient } from '@qdrant/js-client-rest';
import { chromium } from 'playwright';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { EmbeddingService } from './embeddings.js';
import { QdrantWrapper } from './tools/qdrant-client.js';
import { Document } from './types.js';
export interface QdrantCollectionConfig {
params: {
vectors: {
size: number;
distance: string;
};
};
}
export interface QdrantCollectionInfo {
config: QdrantCollectionConfig;
}
export class ApiClient {
qdrantClient: QdrantClient;
private embeddingService: EmbeddingService;
readonly qdrant: QdrantWrapper;
browser: any;
constructor(config: {
embeddingConfig: {
provider: 'ollama' | 'openai';
apiKey?: string;
model?: string;
};
qdrantUrl?: string;
qdrantApiKey?: string;
}) {
this.embeddingService = EmbeddingService.createFromConfig(config.embeddingConfig);
this.qdrant = new QdrantWrapper(config.qdrantUrl, config.qdrantApiKey);
this.qdrantClient = this.qdrant.client;
}
async initBrowser() {
if (!this.browser) {
this.browser = await chromium.launch();
}
}
async cleanup() {
if (this.browser) {
await this.browser.close();
}
}
async getEmbeddings(text: string): Promise<number[]> {
return this.embeddingService.generateEmbeddings(text);
}
get embeddings(): EmbeddingService {
return this.embeddingService;
}
async initCollection(collectionName: string) {
try {
const collections = await this.qdrantClient.getCollections();
const exists = collections.collections.some(c => c.name === collectionName);
const requiredVectorSize = this.embeddingService.getVectorSize();
if (!exists) {
console.error(`Creating new collection with vector size ${requiredVectorSize}`);
await this.createCollection(collectionName, requiredVectorSize);
return;
}
// Verify vector size of existing collection
const collectionInfo = await this.qdrantClient.getCollection(collectionName) as QdrantCollectionInfo;
const currentVectorSize = collectionInfo.config?.params?.vectors?.size;
if (!currentVectorSize) {
console.error('Could not determine current vector size, recreating collection...');
await this.recreateCollection(collectionName, requiredVectorSize);
return;
}
if (currentVectorSize !== requiredVectorSize) {
console.error(`Vector size mismatch: collection=${currentVectorSize}, required=${requiredVectorSize}`);
await this.recreateCollection(collectionName, requiredVectorSize);
}
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('unauthorized')) {
throw new McpError(
ErrorCode.InvalidRequest,
'Failed to authenticate with Qdrant. Please check your API key.'
);
} else if (error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')) {
throw new McpError(
ErrorCode.InternalError,
'Failed to connect to Qdrant. Please check your QDRANT_URL.'
);
}
}
throw new McpError(
ErrorCode.InternalError,
`Failed to initialize Qdrant collection: ${error}`
);
}
}
private async createCollection(collectionName: string, vectorSize: number) {
await this.qdrantClient.createCollection(collectionName, {
vectors: {
size: vectorSize,
distance: 'Cosine',
},
optimizers_config: {
default_segment_number: 2,
memmap_threshold: 20000,
},
replication_factor: 2,
});
// Create indexes for efficient filtering
await this.qdrantClient.createPayloadIndex(collectionName, {
field_name: 'url',
field_schema: 'keyword',
});
await this.qdrantClient.createPayloadIndex(collectionName, {
field_name: 'timestamp',
field_schema: 'datetime',
});
}
private async recreateCollection(collectionName: string, vectorSize: number) {
try {
console.error('Recreating collection with new vector size...');
await this.qdrantClient.deleteCollection(collectionName);
await this.createCollection(collectionName, vectorSize);
console.error(`Collection recreated with new vector size ${vectorSize}`);
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to recreate collection: ${error}`
);
}
}
async isHealthy(): Promise<boolean> {
try {
await this.qdrantClient.getCollections();
return true;
} catch {
return false;
}
}
async addDocument(doc: Document): Promise<void> {
try {
// Check if document already exists
if (await this.qdrant.documentExists(doc.url)) {
throw new McpError(
ErrorCode.InvalidRequest,
`Document with URL ${doc.url} already exists`
);
}
// Generate embeddings for the content
const embedding = await this.embeddingService.generateEmbeddings(doc.content);
// Store document in Qdrant
await this.qdrant.storeDocumentChunks(
[{
content: doc.content,
index: 0,
metadata: {
startPosition: 0,
endPosition: doc.content.length,
isCodeBlock: /```/.test(doc.content)
}
}],
[embedding],
{
url: doc.url,
title: doc.metadata.title || '',
domain: new URL(doc.url).hostname,
timestamp: new Date().toISOString(),
contentType: doc.metadata.contentType || 'text/plain',
wordCount: doc.content.split(/\s+/).length,
hasCode: /```|\bfunction\b|\bclass\b|\bconst\b|\blet\b|\bvar\b/.test(doc.content),
}
);
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to add document: ${error}`
);
}
}
async deleteDocument(url: string): Promise<void> {
try {
await this.qdrant.removeDocument(url);
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to delete document: ${error}`
);
}
}
}