@vessl-ai/mcpctl-control-plane
Version:
Vessl MCP Control Plane service for managing distributed jobs.
203 lines • 7.96 kB
JavaScript
var ServerService_1;
import { __decorate, __metadata } from "tslib";
import { Injectable, Logger, } from '@nestjs/common';
import { TransportType } from '@vessl-ai/mcpctl-shared/types/common';
import { ServerInstanceStatus, } from '@vessl-ai/mcpctl-shared/types/domain/server';
import { generateServerInstanceId, generateServerRunSpecId, } from '@vessl-ai/mcpctl-shared/util';
import { spawn } from 'child_process';
import { AppCacheService } from '../cache/appcache.service';
import { ServerCacheKeys } from '../types/cache';
import { findFreePort } from '../util/network';
let ServerService = ServerService_1 = class ServerService {
appCacheService;
logger = new Logger(ServerService_1.name);
constructor(appCacheService) {
this.appCacheService = appCacheService;
}
instances = {};
async onModuleInit() {
this.logger.log('Initializing server service...');
const instances = await this.appCacheService.get(ServerCacheKeys.INSTANCE);
if (instances) {
this.instances = instances.reduce((acc, instance) => {
acc[instance.id] = instance;
return acc;
}, {});
}
}
async onModuleDestroy() {
this.logger.log('Disposing server service...');
await this.dispose();
}
async start(runSpec) {
runSpec.id = generateServerRunSpecId();
let instance;
switch (runSpec.transport.type) {
case TransportType.Stdio:
instance = await this.startStdioServer(runSpec);
break;
case TransportType.Sse:
instance = await this.startSseServer(runSpec);
break;
case TransportType.StreamableHttp:
instance = await this.startStreamableHttpServer(runSpec);
break;
}
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 startStdioServer(runSpec) {
const serverInstance = {
id: generateServerInstanceId(),
name: runSpec.name,
runSpec,
status: ServerInstanceStatus.Running,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
host: 'localhost',
port: await findFreePort(),
transport: runSpec.transport,
};
const child = spawn('npx', [
'supergateway',
'start',
'--transport',
runSpec.transport.type,
'--port',
serverInstance.port.toString(),
'--baseUrl',
`http://${serverInstance.host}:${serverInstance.port}`,
'--ssePath',
'/sse',
'--messagePath',
'/message',
]);
serverInstance.processId = child.pid;
serverInstance.processHandle = child;
serverInstance.transport = {
type: TransportType.Sse,
};
serverInstance.connectionUrl = `http://${serverInstance.host}:${serverInstance.port}/sse`;
return serverInstance;
}
async startSseServer(runSpec) {
throw new Error('Not implemented');
}
async startStreamableHttpServer(runSpec) {
throw new Error('Not implemented');
}
async upsertRunSpecList(runSpec) {
const runSpecList = await this.appCacheService.get(ServerCacheKeys.RUN_SPEC);
if (!runSpecList) {
await this.appCacheService.set(ServerCacheKeys.RUN_SPEC, [runSpec]);
}
else {
runSpecList.push(runSpec);
await this.appCacheService.set(ServerCacheKeys.RUN_SPEC, runSpecList);
}
}
async upsertInstanceList(instance) {
const instanceList = await this.appCacheService.get(ServerCacheKeys.INSTANCE);
if (!instanceList) {
await this.appCacheService.set(ServerCacheKeys.INSTANCE, [instance]);
}
else {
instanceList.push(instance);
await this.appCacheService.set(ServerCacheKeys.INSTANCE, instanceList);
}
}
async deleteRunSpecList(runSpec) {
const runSpecList = await this.appCacheService.get(ServerCacheKeys.RUN_SPEC);
if (runSpecList) {
runSpecList.splice(runSpecList.indexOf(runSpec), 1);
await this.appCacheService.set(ServerCacheKeys.RUN_SPEC, runSpecList);
}
}
async deleteInstanceList(instance) {
const instanceList = await this.appCacheService.get(ServerCacheKeys.INSTANCE);
if (instanceList) {
instanceList.splice(instanceList.indexOf(instance), 1);
await this.appCacheService.set(ServerCacheKeys.INSTANCE, instanceList);
}
}
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 = 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.appCacheService.get(ServerCacheKeys.INSTANCE);
if (!instances) {
return [];
}
this.logger.debug(`Listing servers: ${JSON.stringify(instances)}`);
return this.sanitizeResponseInstanceList(instances);
}
async listRunSpecs() {
const runSpecs = await this.appCacheService.get(ServerCacheKeys.RUN_SPEC);
this.logger.debug(`Listing servers: ${JSON.stringify(runSpecs)}`);
return runSpecs ?? [];
}
async getInstanceByName(name) {
const instances = await this.appCacheService.get(ServerCacheKeys.INSTANCE);
const result = instances?.find((instance) => instance.name === name);
return result ? this.sanitizeResponseInstance(result) : undefined;
}
async getRunSpecByName(name) {
const runSpecs = await this.appCacheService.get(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(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));
}
};
ServerService = ServerService_1 = __decorate([
Injectable(),
__metadata("design:paramtypes", [AppCacheService])
], ServerService);
export { ServerService };
//# sourceMappingURL=server.service.js.map