Persist jobs, push repos, systemd for generated MCPs

This commit is contained in:
root
2026-03-12 10:21:51 +00:00
parent 6eabd3d3de
commit 91a2f1b602
4 changed files with 110 additions and 3 deletions

View File

@@ -5,6 +5,67 @@ import { loadConfig, ensureDir } from './config.js';
import type { MetaRequest, MetaResponse } from './types.js';
import { updateJob } from './storage.js';
async function createGiteaRepo(name: string) {
const config = loadConfig();
if (!config.gitea) return null;
const owner = config.gitea.owner;
const ownerType = config.gitea.ownerType ?? 'user';
const baseUrl = config.gitea.baseUrl.replace(/\/$/, '');
const apiBase = `${baseUrl}/api/v1`;
const headers = {
'Content-Type': 'application/json',
Authorization: `token ${config.gitea.token}`,
} as const;
const body = JSON.stringify({
name,
private: config.gitea.defaultVisibility !== 'public',
auto_init: false,
});
const url = ownerType === 'org'
? `${apiBase}/orgs/${owner}/repos`
: `${apiBase}/user/repos`;
const res = await fetch(url, { method: 'POST', headers, body });
if (!res.ok) {
const text = await res.text();
throw new Error(`Gitea repo create failed: ${res.status} ${text}`);
}
return `${baseUrl}/${owner}/${name}`;
}
async function gitInitAndPush(repoDir: string, repoUrl: string, token: string) {
const config = loadConfig();
if (config.git?.userName) await run('git', ['config', 'user.name', config.git.userName], repoDir);
if (config.git?.userEmail) await run('git', ['config', 'user.email', config.git.userEmail], repoDir);
await run('git', ['init', '-b', 'main'], repoDir);
await run('git', ['add', '.'], repoDir);
await run('git', ['commit', '-m', 'Initial generated MCP server'], repoDir);
const remote = repoUrl.replace('https://', `https://${encodeURIComponent(token)}@`);
await run('git', ['remote', 'add', 'origin', remote], repoDir);
await run('git', ['push', '-u', 'origin', 'main'], repoDir);
}
function toServiceName(repoName: string) {
return `mcp-${repoName}`.replace(/[^a-zA-Z0-9_.-]/g, '-');
}
async function createSystemdService(repoName: string, repoDir: string, seedPath: string) {
const serviceName = toServiceName(repoName);
const servicePath = `/etc/systemd/system/${serviceName}.service`;
const seed = JSON.parse(fs.readFileSync(seedPath, 'utf8')) as { nsec: string; relays: string[] };
const service = `[Unit]\nDescription=Generated MCP ${repoName}\nAfter=network.target\n\n[Service]\nWorkingDirectory=${repoDir}\nEnvironment=CVMI_SERVE_PRIVATE_KEY=${seed.nsec}\nExecStart=/usr/bin/npx -y cvmi serve -- /usr/bin/node ${repoDir}/dist/server.js\nRestart=always\nRestartSec=3\nStandardOutput=journal\nStandardError=journal\n\n[Install]\nWantedBy=multi-user.target\n`;
fs.writeFileSync(servicePath, service);
await run('systemctl', ['daemon-reload']);
await run('systemctl', ['enable', serviceName]);
await run('systemctl', ['restart', serviceName]);
return serviceName;
}
function run(cmd: string, args: string[], cwd?: string): Promise<string> {
return new Promise((resolve, reject) => {
const child = spawn(cmd, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
@@ -104,10 +165,24 @@ export async function runPipeline(jobId: string, request: MetaRequest): Promise<
updateJob(jobId, { status: 'building', message: `Building ${repoName}` });
await run('npm', ['run', 'build'], repoDir);
// Create repo in Gitea + push
if (config.gitea) {
updateJob(jobId, { status: 'building', message: `Creating repo ${repoName}` });
const repoUrl = await createGiteaRepo(repoName);
if (repoUrl) {
await gitInitAndPush(repoDir, repoUrl, config.gitea.token);
updateJob(jobId, { status: 'building', message: `Pushed ${repoUrl}`, repoUrl });
}
}
const seedPath = config.cvmi?.seedPath ?? '/root/.cvmi/seed.json';
const seedRaw = fs.readFileSync(seedPath, 'utf8');
const seed = JSON.parse(seedRaw) as { nsec: string; relays: string[] };
// write per-repo seed file so systemd can read it
const repoSeedPath = path.join(repoDir, '.cvmi-seed.json');
fs.writeFileSync(repoSeedPath, JSON.stringify(seed, null, 2));
updateJob(jobId, { status: 'running', message: `Starting CVMI for ${repoName}` });
const env = { ...process.env, CVMI_SERVE_PRIVATE_KEY: seed.nsec };
const cvmiArgs = ['-y', 'cvmi', 'serve', '--', 'node', 'dist/server.js'];
@@ -134,6 +209,9 @@ export async function runPipeline(jobId: string, request: MetaRequest): Promise<
updateJob(jobId, { logs: [text] });
});
// create a systemd service to keep the MCP alive
await createSystemdService(repoName, repoDir, repoSeedPath);
return {
status: 'running',
message: `Started ${repoName}. Awaiting pubkey...`,