UNPKG

@vessl-ai/mcpctl-control-plane

Version:

Vessl MCP Control Plane service for managing distributed jobs.

326 lines 13.7 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var ServerService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.ServerService = void 0; const common_1 = require("@nestjs/common"); const config_1 = require("@nestjs/config"); const common_2 = require("@vessl-ai/mcpctl-shared/types/common"); const server_1 = require("@vessl-ai/mcpctl-shared/types/domain/server"); const util_1 = require("@vessl-ai/mcpctl-shared/util"); const child_process_1 = require("child_process"); const fs = require("fs"); const path = require("path"); const appcache_service_1 = require("../cache/appcache.service"); const secret_service_1 = require("../secret/secret.service"); const cache_1 = require("../types/cache"); const network_1 = require("../util/network"); let ServerService = ServerService_1 = class ServerService { appCacheService; configService; secretService; logger = new common_1.Logger(ServerService_1.name); constructor(appCacheService, configService, secretService) { this.appCacheService = appCacheService; this.configService = configService; this.secretService = secretService; } instances = {}; async onModuleInit() { this.logger.log('Initializing server service...'); const instances = await this.appCacheService.get(cache_1.ServerCacheKeys.INSTANCE); if (instances) { this.instances = instances; } } async onModuleDestroy() { this.logger.log('Disposing server service...'); await this.dispose(); } async getServerInstances() { if (Object.keys(this.instances).length > 0) { return this.instances; } const instances = await this.appCacheService.get(cache_1.ServerCacheKeys.INSTANCE); this.instances = instances ?? {}; return this.instances; } async setServerInstances(instances) { this.instances = instances; await this.appCacheService.set(cache_1.ServerCacheKeys.INSTANCE, this.instances); } createLogFile(params) { const { instanceId } = params; const logDir = this.configService.get('app.logDir'); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } const logFile = path.join(logDir, `${instanceId}.log`); return Promise.resolve({ logFile }); } async start(runSpec) { runSpec.id = (0, util_1.generateServerRunSpecId)(); let instance = await this.initializeServerInstance(runSpec); instance = await this.startServer(instance); this.instances[instance.id] = instance; await this.upsertRunSpecList(runSpec); await this.upsertInstanceList(instance); this.logger.debug(`Started server ${instance.id} with spec ${JSON.stringify(runSpec)}`); return this.sanitizeResponseInstance(instance); } async resolveSecret(secrets) { if (!secrets) { return {}; } const resolvedSecret = {}; for (const [key, secretRef] of Object.entries(secrets)) { const secret = await this.secretService.get(secretRef.source, secretRef.key); resolvedSecret[key] = secret; } return resolvedSecret; } async initializeServerInstance(runSpec) { const port = runSpec.transport.port ?? (await (0, network_1.findFreePort)()); const serverInstance = { id: (0, util_1.generateServerInstanceId)(), name: runSpec.name, runSpec, status: server_1.ServerInstanceStatus.Running, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), host: runSpec.transport.host ?? 'localhost', port, transport: runSpec.transport, }; const { logFile } = await this.createLogFile({ instanceId: serverInstance.id, }); const resolvedSecret = await this.resolveSecret(serverInstance.runSpec.secrets); this.logger.debug(`Original secret: ${JSON.stringify(serverInstance.runSpec.secrets)}, Resolved secret: ${JSON.stringify(resolvedSecret)}`); const env = { ...serverInstance.runSpec.env, ...resolvedSecret, }; if (process.env.PATH) { env['PATH'] = process.env.PATH; } serverInstance.logsPath = logFile; serverInstance.env = env; return serverInstance; } async startServer(serverInstance) { let child; if (serverInstance.transport.type === common_2.TransportType.Stdio) { child = await this.startStdioServer(serverInstance); } else if (serverInstance.transport.type === common_2.TransportType.Sse) { child = await this.startSseServer(serverInstance); } else if (serverInstance.transport.type === common_2.TransportType.StreamableHttp) { child = await this.startStreamableHttpServer(serverInstance); } if (!child) { throw new Error(`Failed to start server ${serverInstance.id}`); } this.logger.debug(`Started server ${serverInstance.id} with args: ${child.spawnargs}, env: ${JSON.stringify(serverInstance.env)}`); child.on('error', (error) => { this.logger.error(`Failed to start server ${serverInstance.id}: ${error}`); }); if (serverInstance.logsPath) { const logFileStream = fs.createWriteStream(serverInstance.logsPath); if (child.stdout) { child.stdout.pipe(logFileStream); } if (child.stderr) { child.stderr.pipe(logFileStream); } } child.on('close', async (code, signal) => { this.logger.log(`Server ${serverInstance.id} closed with code ${code}`); const instance = (await this.getServerInstances())[serverInstance.id]; if (instance) { instance.status = server_1.ServerInstanceStatus.Stopped; instance.updatedAt = new Date().toISOString(); await this.setServerInstances(this.instances); await this.deleteRunSpecList(instance.runSpec); } this.logger.log(`Server ${serverInstance.id} stopped`); }); serverInstance.processId = child.pid; serverInstance.processHandle = child; return serverInstance; } async startStdioServer(serverInstance) { const child = (0, child_process_1.spawn)('npx', [ '-y', 'supergateway', '--stdio', serverInstance.runSpec.command, '--port', serverInstance.port.toString(), '--baseUrl', `http://${serverInstance.host}:${serverInstance.port}`, '--ssePath', '/sse', '--messagePath', '/message', ], { env: serverInstance.env, }); serverInstance.transport = { type: common_2.TransportType.Sse, }; serverInstance.connectionUrl = `http://${serverInstance.host}:${serverInstance.port}/sse`; return child; } async startSseServer(serverInstance) { const [command, ...args] = serverInstance.runSpec.command.split(' '); const child = (0, child_process_1.spawn)(command, args, { env: serverInstance.env, }); serverInstance.transport = { type: common_2.TransportType.Sse, }; serverInstance.connectionUrl = `http://${serverInstance.host}:${serverInstance.port}/sse`; return child; } async startStreamableHttpServer(serverInstance) { throw new Error('Not implemented'); } async upsertRunSpecList(runSpec) { const runSpecList = await this.appCacheService.get(cache_1.ServerCacheKeys.RUN_SPEC); if (!runSpecList) { await this.appCacheService.set(cache_1.ServerCacheKeys.RUN_SPEC, [runSpec]); } else { runSpecList.push(runSpec); await this.appCacheService.set(cache_1.ServerCacheKeys.RUN_SPEC, runSpecList); } } async upsertInstanceList(instance) { const instances = await this.getServerInstances(); if (Object.keys(instances).length === 0) { await this.setServerInstances({ [instance.id]: instance, }); } else { instances[instance.id] = instance; await this.setServerInstances(instances); } } async deleteRunSpecList(runSpec) { const runSpecList = await this.appCacheService.get(cache_1.ServerCacheKeys.RUN_SPEC); if (runSpecList) { runSpecList.splice(runSpecList.indexOf(runSpec), 1); await this.appCacheService.set(cache_1.ServerCacheKeys.RUN_SPEC, runSpecList); } } async deleteInstanceList(instance) { const instances = await this.getServerInstances(); if (instances) { delete this.instances[instance.id]; await this.setServerInstances(this.instances); } } async stopInstance(name) { const instance = await this.getInstanceByName(name); if (!instance) { throw new Error(`Instance ${name} not found`); } const instanceProcessHandle = this.instances[instance.id].processHandle; if (instanceProcessHandle) { instanceProcessHandle.kill(); } else { if (instance.processId) { try { process.kill(instance.processId); } catch (error) { this.logger.error(`Failed to kill process ${instance.processId}`); } } } instance.status = server_1.ServerInstanceStatus.Stopped; await this.upsertInstanceList(instance); return this.sanitizeResponseInstance(instance); } async restartInstance(name) { const instance = await this.stopInstance(name); return this.start(instance.runSpec); } async listInstances() { const instances = await this.getServerInstances(); if (!instances) { return []; } this.logger.debug(`Listing servers: ${JSON.stringify(instances)}`); return this.sanitizeResponseInstanceList(Object.values(instances)); } async listRunSpecs() { const runSpecs = await this.appCacheService.get(cache_1.ServerCacheKeys.RUN_SPEC); this.logger.debug(`Listing servers: ${JSON.stringify(runSpecs)}`); return runSpecs ?? []; } async getInstanceByName(name) { const instances = await this.getServerInstances(); const result = Object.values(instances).find((instance) => instance.name === name); return result ? this.sanitizeResponseInstance(result) : undefined; } async getRunSpecByName(name) { const runSpecs = await this.appCacheService.get(cache_1.ServerCacheKeys.RUN_SPEC); return runSpecs?.find((runSpec) => runSpec.name === name); } async dispose() { this.logger.log('Stopping all instances...'); const instances = await this.listInstances(); for (const instance of instances) { await this.stopInstance(instance.name); } this.logger.log('All instances stopped'); this.logger.log('Backing up cache...'); const cache = await this.appCacheService.get(cache_1.ServerCacheKeys.RUN_SPEC); this.logger.log('Cache backed up'); this.logger.log('Clearing cache...'); await this.appCacheService.clear(); } sanitizeResponseInstance(instance) { return { ...instance, processHandle: undefined, }; } sanitizeResponseInstanceList(instances) { return instances.map((instance) => this.sanitizeResponseInstance(instance)); } async removeServerInstance(serverName) { const instances = await this.getServerInstances(); const instance = Object.values(instances).find((instance) => instance.name === serverName); if (!instance) { throw new Error(`Server ${serverName} not found`); } if (instance.status === server_1.ServerInstanceStatus.Running) { throw new Error(`Server ${serverName} is running, please stop it first`); } delete instances[serverName]; await this.setServerInstances(instances); await this.deleteRunSpecList(instance.runSpec); } }; exports.ServerService = ServerService; exports.ServerService = ServerService = ServerService_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [appcache_service_1.AppCacheService, config_1.ConfigService, secret_service_1.SecretService]) ], ServerService); //# sourceMappingURL=server.service.js.map