import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import path from 'path';
export interface SearchIntent {
filters?: string;
searchQuery?: string;
indexName?: string;
}
export interface SearchResult {
results: any[];
nbHits: number;
processingTimeMS: number;
}
export class AlgoliaMCPClient {
private client: Client | null = null;
private transport: StdioClientTransport | null = null;
private isConnected = false;
constructor() {}
async connect(): Promise<void> {
if (this.isConnected) {
return;
}
try {
// Validate environment variables
if (!process.env.ALGOLIA_APP_ID || !process.env.ALGOLIA_API_KEY) {
throw new Error('ALGOLIA_APP_ID and ALGOLIA_API_KEY environment variables are required');
}
// Path to the MCP server executable
const mcpServerPath = path.resolve(__dirname, '../../mcp-node/src/app.ts');
console.log('Starting MCP server process...');
// Create transport for communication - it will handle the process spawning
this.transport = new StdioClientTransport({
command: 'node',
args: [
'--experimental-strip-types',
'--no-warnings=ExperimentalWarning',
mcpServerPath
],
env: {
...process.env,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID,
ALGOLIA_API_KEY: process.env.ALGOLIA_API_KEY
}
});
// Create MCP client
this.client = new Client({
name: 'algolia-backend-client',
version: '1.0.0'
}, {
capabilities: {}
});
// Connect to the server with timeout
const connectPromise = this.client.connect(this.transport);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('MCP connection timeout')), 10000);
});
await Promise.race([connectPromise, timeoutPromise]);
this.isConnected = true;
// Handle transport errors
if (this.transport) {
this.transport.onclose = () => {
console.log('MCP transport closed');
this.isConnected = false;
this.handleConnectionLoss();
};
this.transport.onerror = (error) => {
console.error('MCP transport error:', error);
this.isConnected = false;
this.handleConnectionLoss();
};
}
console.log('Successfully connected to Algolia MCP server');
} catch (error) {
console.error('Failed to connect to MCP server:', error);
await this.cleanup();
throw error;
}
}
async disconnect(): Promise<void> {
await this.cleanup();
console.log('Disconnected from Algolia MCP server');
}
private async handleConnectionLoss(): Promise<void> {
console.log('Handling MCP connection loss...');
await this.cleanup();
// Optional: Attempt reconnection after a delay
setTimeout(async () => {
if (!this.isConnected) {
console.log('Attempting to reconnect to MCP server...');
try {
await this.connect();
} catch (error) {
console.error('Failed to reconnect to MCP server:', error);
}
}
}, 5000);
}
private async cleanup(): Promise<void> {
if (this.client) {
try {
await this.client.close();
} catch (error) {
console.error('Error closing MCP client:', error);
}
this.client = null;
}
if (this.transport) {
try {
await this.transport.close();
} catch (error) {
console.error('Error closing MCP transport:', error);
}
this.transport = null;
}
this.isConnected = false;
}
async listIndices(applicationId?: string): Promise<any> {
if (!this.client || !this.isConnected) {
throw new Error('MCP client not connected');
}
try {
const result = await this.client.callTool({
name: 'listIndices',
arguments: { applicationId: applicationId || process.env.ALGOLIA_APP_ID }
});
return result;
} catch (error) {
console.error('Error listing indices:', error);
if (error instanceof Error && error.message.includes('connection')) {
this.isConnected = false;
this.handleConnectionLoss();
}
throw error;
}
}
async searchIndex(params: {
indexName: string;
query?: string;
filters?: string;
hitsPerPage?: number;
applicationId?: string;
}): Promise<SearchResult> {
if (!this.client || !this.isConnected) {
throw new Error('MCP client not connected');
}
try {
const requestBody: any = {
query: params.query || '',
hitsPerPage: params.hitsPerPage || 10
};
if (params.filters) {
requestBody.filters = params.filters;
}
const result = await this.client.callTool({
name: 'searchSingleIndex',
arguments: {
applicationId: params.applicationId || process.env.ALGOLIA_APP_ID,
indexName: params.indexName,
requestBody: requestBody
}
});
// Check if the MCP call returned an error
if (result.isError) {
const errorText = result.content && Array.isArray(result.content) && result.content[0] && 'text' in result.content[0]
? result.content[0].text
: 'Unknown error';
throw new Error(`MCP server error: ${errorText}`);
}
// Parse the result from the MCP server
if (result.content && Array.isArray(result.content) && result.content[0] && 'text' in result.content[0]) {
const responseText = result.content[0].text;
// console.log('MCP response:', responseText); // Debug log
try {
const searchResult = JSON.parse(responseText);
return {
results: searchResult.hits || [],
nbHits: searchResult.nbHits || 0,
processingTimeMS: searchResult.processingTimeMS || 0
};
} catch (parseError) {
console.error('Failed to parse MCP response as JSON:', responseText);
throw new Error(`Invalid response from MCP server: ${responseText.substring(0, 100)}...`);
}
}
return {
results: [],
nbHits: 0,
processingTimeMS: 0
};
} catch (error) {
console.error('Error searching index:', error);
if (error instanceof Error && error.message.includes('connection')) {
this.isConnected = false;
this.handleConnectionLoss();
}
throw error;
}
}
async getApplications(): Promise<any> {
if (!this.client || !this.isConnected) {
throw new Error('MCP client not connected');
}
try {
const result = await this.client.callTool({
name: 'getApplications',
arguments: {}
});
return result;
} catch (error) {
console.error('Error getting applications:', error);
if (error instanceof Error && error.message.includes('connection')) {
this.isConnected = false;
this.handleConnectionLoss();
}
throw error;
}
}
isReady(): boolean {
return this.isConnected && this.client !== null;
}
}
// Create a singleton instance
export const mcpClient = new AlgoliaMCPClient();