RagDocs MCP Server
- src
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import axios from 'axios';
import { ApiClient } from './api-client.js';
import { SearchDocumentationHandler } from './handlers/search-documentation.js';
import { ListDocumentationHandler } from './handlers/list-documentation.js';
import { ListOptions } from './tools/list-utils.js';
import { Document } from './types.js';
// Force using IP address to avoid hostname resolution issues
const QDRANT_URL = process.env.QDRANT_URL || 'http://127.0.0.1:6333';
const QDRANT_API_KEY = process.env.QDRANT_API_KEY;
const EMBEDDING_PROVIDER = process.env.EMBEDDING_PROVIDER || 'ollama';
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
// Test connection with direct axios call first
try {
const response = await axios.get(`${QDRANT_URL}/collections`);
console.error('Successfully connected to Qdrant:', response.data);
} catch (error) {
console.error('Failed to connect to Qdrant:', error);
throw new McpError(
ErrorCode.InternalError,
'Failed to establish initial connection to Qdrant server'
);
}
const client = new ApiClient({
qdrantUrl: QDRANT_URL,
qdrantApiKey: QDRANT_API_KEY,
embeddingConfig: {
provider: EMBEDDING_PROVIDER as 'ollama' | 'openai',
apiKey: OPENAI_API_KEY,
model: EMBEDDING_PROVIDER === 'ollama' ? 'nomic-embed-text' : 'text-embedding-3-small'
}
});
try {
// Initialize Qdrant collection
await client.qdrant.initializeCollection();
console.error('Successfully initialized Qdrant collection');
} catch (error) {
console.error('Failed to initialize Qdrant collection:', error);
throw error;
}
class RagDocsServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: 'ragdocs',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
this.server.onerror = (error) => console.error('[MCP Error]', error);
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'add_document',
description: 'Add a document to the RAG system',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'Document URL' },
content: { type: 'string', description: 'Document content' },
metadata: {
type: 'object',
properties: {
title: { type: 'string', description: 'Document title' },
contentType: { type: 'string', description: 'Content type (e.g., text/plain, text/markdown)' },
},
additionalProperties: true,
},
},
required: ['url', 'content'],
},
},
{
name: 'search_documents',
description: 'Search for documents using semantic similarity',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Natural language search query'
},
options: {
type: 'object',
description: 'Search options',
properties: {
limit: {
type: 'number',
description: 'Maximum number of results (1-20)',
minimum: 1,
maximum: 20
},
scoreThreshold: {
type: 'number',
description: 'Minimum similarity score (0-1)',
minimum: 0,
maximum: 1
},
filters: {
type: 'object',
description: 'Optional filters',
properties: {
domain: {
type: 'string',
description: 'Filter by domain'
},
hasCode: {
type: 'boolean',
description: 'Filter for documents containing code'
},
after: {
type: 'string',
description: 'Filter for documents after date (ISO format)'
},
before: {
type: 'string',
description: 'Filter for documents before date (ISO format)'
}
}
}
}
}
},
required: ['query'],
},
},
{
name: 'delete_document',
description: 'Delete a document from the RAG system',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'Document URL to delete' },
},
required: ['url'],
},
},
{
name: 'list_documents',
description: 'List all stored documents with pagination and grouping options',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number (default: 1)',
minimum: 1
},
pageSize: {
type: 'number',
description: 'Number of documents per page (default: 20)',
minimum: 1,
maximum: 100
},
groupByDomain: {
type: 'boolean',
description: 'Group documents by domain (default: false)'
},
sortBy: {
type: 'string',
description: 'Sort field (default: timestamp)',
enum: ['timestamp', 'title', 'domain']
},
sortOrder: {
type: 'string',
description: 'Sort order (default: desc)',
enum: ['asc', 'desc']
}
}
}
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
switch (request.params.name) {
case 'add_document': {
const args = request.params.arguments as Record<string, unknown>;
if (!args || typeof args.url !== 'string' || typeof args.content !== 'string') {
throw new Error('Invalid document format: url and content must be strings');
}
const doc: Document = {
url: args.url,
content: args.content,
metadata: (args.metadata as Record<string, unknown>) || {}
};
await client.addDocument(doc);
return {
content: [{ type: 'text', text: `Document ${doc.url} added successfully` }],
};
}
case 'search_documents': {
const { query, options } = request.params.arguments as {
query: string;
options?: {
limit?: number;
scoreThreshold?: number;
filters?: {
domain?: string;
hasCode?: boolean;
after?: string;
before?: string;
};
};
};
const searchHandler = new SearchDocumentationHandler(
client.qdrant,
client.embeddings,
this.server,
client
);
return await searchHandler.handle({ query, options });
}
case 'delete_document': {
const { url } = request.params.arguments as { url: string };
await client.deleteDocument(url);
return {
content: [{ type: 'text', text: `Document ${url} deleted successfully` }],
};
}
case 'list_documents': {
const args = request.params.arguments as ListOptions;
const listHandler = new ListDocumentationHandler(this.server, client);
return await listHandler.handle(args || {});
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
console.error('[Tool Error]', errorMessage);
return {
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
isError: true,
};
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('RagDocs MCP server running on stdio');
}
}
const server = new RagDocsServer();
server.run().catch(console.error);