Skip to main content
Glama
ws-ui.ts8.42 kB
import { UI_ClientBoundMessage, UI_ServerBoundMessage, UpdateTargetServerRequest, WS_CONNECTION_ERROR, applyParsedAppConfigRequestSchema, createTargetServerRequestSchema, } from "@mcpx/shared-model"; import { UIConnection } from "../services/connections.js"; import { Server as HTTPServer } from "http"; import { Socket, Server as WSServer } from "socket.io"; import { Logger } from "winston"; import { Services } from "../services/services.js"; import { loggableError } from "@mcpx/toolkit-core/logging"; import { env } from "../env.js"; import { checkHubConnection } from "./hub-connection-guard.js"; import { ConfigSnapshot } from "../config.js"; import { stringify } from "yaml"; export function bindUIWebsocket( server: HTTPServer, services: Services, logger: Logger, ): void { const io = new WSServer(server, { path: "/ws-ui", cors: { origin: env.CORS_ORIGINS || "*", credentials: true, }, }); // Middleware to check hub connection before allowing websocket connections io.use((socket, next) => { const connectionCheck = checkHubConnection( services.hubService, env.ENFORCE_HUB_CONNECTION, ); if (!connectionCheck.allowed) { logger.warn("WebSocket connection rejected - hub not connected", { id: socket.id, status: connectionCheck.status, connectionError: connectionCheck.connectionError?.toJSON(), }); const err = new Error(WS_CONNECTION_ERROR.HUB_NOT_CONNECTED); return next(err); } next(); }); io.on("connection", (socket) => { logger.debug("WebSocket connection established", { id: socket.id, }); const systemStateCallback = services.controlPlane.subscribeToSystemStateUpdates((systemState) => { socket.emit(UI_ClientBoundMessage.SystemState, systemState); }); const appConfigCallback = services.controlPlane.subscribeToAppConfigUpdates( (configSnapshot: ConfigSnapshot) => { // Convert ConfigSnapshot to SerializedAppConfig const yaml = stringify(configSnapshot.config); socket.emit(UI_ClientBoundMessage.AppConfig, { yaml, version: configSnapshot.version, lastModified: configSnapshot.lastModified, }); }, ); services.connections.addSession( new UIConnection(socket, systemStateCallback, appConfigCallback), ); logger.debug("UI sessions updated", { totalSessions: services.connections.size(), allSessionIds: services.connections.getSessionIds(), }); socket.on("disconnect", () => { services.connections.removeSession(socket.id); logger.debug("UI disconnected:", { id: socket.id, totalSessions: services.connections.size(), remainingSessions: services.connections.getSessionIds(), }); }); // Handle events from UI Object.entries(UI_ServerBoundMessage).forEach(([_, eventName]) => { socket.on(eventName, async (payload) => { handleWsEvent(services, logger, socket, eventName, payload); }); }); }); } async function handleWsEvent( services: Services, logger: Logger, socket: Socket, eventName: UI_ServerBoundMessage, payload: unknown, ): Promise<void> { logger.debug(`Received event: ${eventName}`, { payload: payload, id: socket.id, }); try { switch (eventName) { case UI_ServerBoundMessage.GetAppConfig: { logger.debug("Fetching current app config"); const appConfig = services.controlPlane.getAppConfig(); socket.emit(UI_ClientBoundMessage.AppConfig, appConfig); break; } case UI_ServerBoundMessage.GetSystemState: { logger.debug("Fetching current system state"); const systemState = services.controlPlane.getSystemState(); socket.emit(UI_ClientBoundMessage.SystemState, systemState); break; } case UI_ServerBoundMessage.PatchAppConfig: { logger.debug("Patching app config"); try { // Validate and parse the raw YAML payload const parseResult = applyParsedAppConfigRequestSchema.safeParse(payload); if (!parseResult.success) { logger.error("Invalid raw app config request", { error: parseResult.error, payload: payload, }); socket.emit(UI_ClientBoundMessage.PatchAppConfigFailed, { error: `Invalid request format: ${parseResult.error.message}`, }); break; } // The schema already parsed the YAML, so we can use it directly const parsedConfig = parseResult.data; await services.controlPlane.patchAppConfig(parsedConfig); // Send back the updated config const updatedConfig = services.controlPlane.getAppConfig(); socket.emit(UI_ClientBoundMessage.AppConfig, updatedConfig); } catch (e) { const error = loggableError(e); logger.error("Failed to patch app config", { error, payload: payload, }); socket.emit(UI_ClientBoundMessage.PatchAppConfigFailed, { error: error.errorMessage, }); } break; } case UI_ServerBoundMessage.AddTargetServer: { logger.debug("Adding target server"); const parseResult = createTargetServerRequestSchema.safeParse(payload); if (!parseResult.success) { logger.error("Invalid target server payload", { error: parseResult.error, payload, }); socket.emit(UI_ClientBoundMessage.AddTargetServerFailed, { error: "Invalid server configuration", }); break; } const targetServerPayload = parseResult.data; try { const result = await services.controlPlane.addTargetServer(targetServerPayload); socket.emit(UI_ClientBoundMessage.TargetServerAdded, result); // System state will be automatically broadcast via subscription } catch (e) { const error = loggableError(e); logger.error("Failed to add target server", { error, payload: targetServerPayload, }); socket.emit(UI_ClientBoundMessage.AddTargetServerFailed, { error: error.errorMessage, }); } break; } case UI_ServerBoundMessage.RemoveTargetServer: { logger.debug("Removing target server"); const removePayload = payload as { name: string }; try { await services.controlPlane.removeTargetServer(removePayload.name); socket.emit(UI_ClientBoundMessage.TargetServerRemoved, removePayload); // System state will be automatically broadcast via subscription } catch (e) { const error = loggableError(e); logger.error("Failed to remove target server", { error, payload: removePayload, }); socket.emit(UI_ClientBoundMessage.RemoveTargetServerFailed, { error: error.errorMessage, }); } break; } case UI_ServerBoundMessage.UpdateTargetServer: { logger.debug("Updating target server"); const { name, data } = payload as { name: string; data: UpdateTargetServerRequest; }; try { const result = await services.controlPlane.updateTargetServer( name, data, ); socket.emit(UI_ClientBoundMessage.TargetServerUpdated, result); // System state will be automatically broadcast via subscription } catch (e) { const error = loggableError(e); logger.error("Failed to update target server", { error, name, data }); socket.emit(UI_ClientBoundMessage.UpdateTargetServerFailed, { error: error.errorMessage, }); } break; } default: { logger.warn(`Unhandled event: ${eventName}`, { id: socket.id, payload: payload, }); break; } } } catch (e) { const error = loggableError(e); logger.error(`Error handling event: ${eventName}`, { error, id: socket.id, payload: payload, }); } logger.debug(`Handled event: ${eventName}`, { id: socket.id }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TheLunarCompany/lunar'

If you have feedback or need assistance with the MCP directory API, please join our Discord server