UNPKG

@smartbear/mcp

Version:

MCP server for interacting SmartBear Products

129 lines (128 loc) 5.1 kB
import { ZodOptional, ZodString } from "zod"; /** * Central registry for all MCP clients * Add new clients here to make them automatically available */ class ClientRegistry { entries = []; enabledClients = null; /** * Configure which clients should be enabled based on MCP_CLIENTS env var * If not set or empty, all clients are enabled * If set, should be comma-separated list of client names (case-insensitive) */ constructor() { const enabledClientsEnv = process.env.MCP_CLIENTS?.trim(); if (!enabledClientsEnv) { // Empty or not set = all clients enabled this.enabledClients = null; return; } // Parse comma-separated list and normalize to lowercase for comparison this.enabledClients = new Set(enabledClientsEnv .split(",") .map((name) => name.trim().toLowerCase()) .filter((name) => name.length > 0)); } /** * Check if a client is enabled based on MCP_CLIENTS configuration */ isClientEnabled(name) { if (this.enabledClients === null) { return true; // All clients enabled } return this.enabledClients.has(name.toLowerCase()); } /** * Validate if a config option is an Allowed Endpoint URL * Supports both exact matches and regex patterns * Patterns starting with / and ending with / are treated as regex * @param zodType The Zod type definition for the config option * @param value The actual config value to validate */ validateAllowedEndpoint(zodType, value) { if (zodType instanceof ZodOptional) { zodType = zodType._def.innerType; } if (zodType instanceof ZodString) { if (zodType.isURL) { const allowedEndpoints = process.env.MCP_ALLOWED_ENDPOINTS?.split(","); if (allowedEndpoints) { for (const endpoint of allowedEndpoints) { const trimmedEndpoint = endpoint.trim(); // Check if this is a regex pattern (wrapped in /) if (trimmedEndpoint.startsWith("/") && trimmedEndpoint.endsWith("/")) { try { const pattern = trimmedEndpoint.slice(1, -1); // Remove leading/trailing / const regex = new RegExp(pattern); if (regex.test(value)) { return; } } catch (error) { console.warn(`Invalid regex pattern in MCP_ALLOWED_ENDPOINTS: ${trimmedEndpoint}, error: ${error}`); } } else { // Exact match if (value === trimmedEndpoint) { return; } } } throw new Error(`URL ${value} is not allowed`); } } } } /** * Register a client class * @param name Display name for the client (for logging) */ register(client) { this.entries.push(client); } /** * Get all registered clients (filtered by MCP_CLIENTS if configured) */ getAll() { return this.entries.filter((entry) => this.isClientEnabled(entry.name)); } /** * Configures all enabled clients on the given MCP server * @param server The MCP server on which the client is registered * @param getConfigValue A function that obtains a configuration value for the given client and requirement name * @returns The number of clients successfully configured */ async configure(server, getConfigValue) { let configuredCount = 0; entryLoop: for (const entry of this.getAll()) { const config = {}; for (const configKey of Object.keys(entry.config.shape)) { const value = getConfigValue(entry, configKey); if (value !== null) { // validate if a config option is an Allowed Endpoint URL this.validateAllowedEndpoint(entry.config.shape[configKey], value); config[configKey] = value; } else if (!entry.config.shape[configKey].isOptional()) { continue entryLoop; // Skip configuring this client - missing required config } } if (await entry.configure(server, config)) { server.addClient(entry); configuredCount++; } } return configuredCount; } /** * Clear all registrations (useful for testing) */ clear() { this.entries = []; } } // Create and export the singleton registry export const clientRegistry = new ClientRegistry();