@1mcp/agent
Version:
One MCP server to aggregate them all - A unified Model Context Protocol server implementation
138 lines (137 loc) • 5.53 kB
JavaScript
import createClient from '../client.js';
import logger from '../logger/logger.js';
import { CONNECTION_RETRY, MCP_SERVER_NAME, ERROR_CODES } from '../constants.js';
import { ClientConnectionError, ClientNotFoundError, MCPError } from '../utils/errorTypes.js';
import { ClientStatus } from '../types.js';
/**
* Creates client instances for all transports with retry logic
* @param transports Record of transport instances
* @returns Record of client instances
*/
export async function createClients(transports) {
const clients = {};
for (const [name, transport] of Object.entries(transports)) {
logger.info(`Creating client for ${name}`);
try {
const client = await createClient();
// Connect with retry logic
await connectWithRetry(client, transport, name);
clients[name] = {
name,
transport,
client,
status: ClientStatus.Connected,
lastConnected: new Date(),
};
logger.info(`Client created for ${name}`);
}
catch (error) {
logger.error(`Failed to create client for ${name}: ${error}`);
clients[name] = {
name,
transport,
client: await createClient(),
status: ClientStatus.Error,
lastError: error instanceof Error ? error : new Error(String(error)),
};
}
}
return Object.freeze(clients);
}
/**
* Connects a client to its transport with retry logic
* @param client The client to connect
* @param transport The transport to connect to
* @param name The name of the client for logging
*/
async function connectWithRetry(client, transport, name) {
let retryDelay = CONNECTION_RETRY.INITIAL_DELAY_MS;
for (let i = 0; i < CONNECTION_RETRY.MAX_ATTEMPTS; i++) {
try {
await client.connect(transport);
const sv = await client.getServerVersion();
if (sv?.name === MCP_SERVER_NAME) {
throw new ClientConnectionError(name, new Error('Aborted to prevent circular dependency'));
}
logger.info(`Successfully connected to ${name} with server ${sv?.name} version ${sv?.version}`);
return;
}
catch (error) {
logger.error(`Failed to connect to ${name}: ${error}`);
if (i < CONNECTION_RETRY.MAX_ATTEMPTS - 1) {
logger.info(`Retrying in ${retryDelay}ms...`);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
retryDelay *= 2; // Exponential backoff
}
else {
throw new ClientConnectionError(name, error instanceof Error ? error : new Error(String(error)));
}
}
}
}
/**
* Gets a client by name with error handling
* @param clients Record of client instances
* @param clientName The name of the client to get
* @returns The client instance
* @throws ClientNotFoundError if the client is not found
*/
export function getClient(clients, clientName) {
const client = clients[clientName];
if (!client) {
throw new ClientNotFoundError(clientName);
}
return client;
}
/**
* Executes an operation with error handling and retry logic
* @param operation The operation to execute
* @param context The execution context (client info or server info)
* @param options Operation options including timeout and retry settings
* @returns The result of the operation
*/
export async function executeOperation(operation, contextName, options = {}) {
const { retryCount = 0, retryDelay = 1000 } = options;
let lastError;
for (let i = 0; i <= retryCount; i++) {
try {
return await operation();
}
catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (i < retryCount) {
logger.info(`Retrying operation ${operation.name} on ${contextName} after ${retryDelay}ms`);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
}
// If we get here, we've exhausted all retries
logger.error(`Operation failed on ${contextName} after ${retryCount + 1} attempts: ${lastError}`);
if (lastError instanceof MCPError) {
throw lastError;
}
const mcpError = new MCPError(`Error executing operation on ${contextName}`, ERROR_CODES.INTERNAL_SERVER_ERROR, {
originalError: lastError,
});
throw mcpError;
}
/**
* Executes a client operation with error handling and retry logic
* @param clients Record of client instances
* @param clientName The name of the client to use
* @param operation The operation to execute
* @param options Operation options including timeout and retry settings
*/
export async function executeClientOperation(clients, clientName, operation, options = {}) {
const clientInfo = getClient(clients, clientName);
return executeOperation(() => operation(clientInfo), `client ${clientName}`, options);
}
/**
* Executes a server operation with error handling and retry logic
* @param server The server to execute the operation on
* @param operation The operation to execute
* @param options Operation options including timeout and retry settings
*/
export async function executeServerOperation(server, operation, options = {}) {
return executeOperation(() => operation(server), 'server', options);
}