@vessl-ai/mcpctl-control-plane
Version:
Vessl MCP Control Plane service for managing distributed jobs.
326 lines • 13.7 kB
JavaScript
"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