@smartbear/mcp
Version:
MCP server for interacting SmartBear Products
129 lines (128 loc) • 5.1 kB
JavaScript
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();