Skip to main content
Glama
db-versioning.ts8.62 kB
/** * Database versioning and update system * Manages version manifests, downloads, and integrity verification */ import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; export interface DbVersionManifest { version: string; timestamp: string; type: 'incremental' | 'full'; hash: string; size: number; downloadUrl: string; changelog: string; } export class DbVersioning { private localManifestPath: string; private dbPath: string; private dataDir: string; private remoteRepoUrl: string; constructor(dbPath?: string) { this.dbPath = dbPath || path.join(process.cwd(), 'data', 'mcmodding-docs.db'); this.dataDir = path.dirname(this.dbPath); this.localManifestPath = path.join(this.dataDir, 'db-manifest.json'); // Extract owner/repo from process.env or use defaults this.remoteRepoUrl = process.env.GITHUB_REPO_URL || 'https://api.github.com/repos/OGMatrix/mcmodding-mcp'; } /** * Get local manifest or create default */ getLocalManifest(): DbVersionManifest | null { try { if (!fs.existsSync(this.localManifestPath)) { return null; } const content = fs.readFileSync(this.localManifestPath, 'utf-8'); return JSON.parse(content) as DbVersionManifest; } catch (error) { console.error('[DbVersioning] Error reading local manifest:', error); return null; } } /** * Fetch remote manifest from GitHub releases */ async getRemoteManifest(): Promise<DbVersionManifest | null> { try { // Fetch latest release from GitHub API const releaseUrl = `${this.remoteRepoUrl}/releases/latest`; const response = await fetch(releaseUrl, { headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'mcmodding-mcp', }, }); if (!response.ok) { console.error(`[DbVersioning] Failed to fetch releases: ${response.status}`); return null; } const release = (await response.json()) as { name: string; tag_name: string; assets: Array<{ name: string; browser_download_url: string }>; body: string; }; // Look for manifest in release assets or body const manifestAsset = release.assets.find((a) => a.name === 'db-manifest.json'); if (!manifestAsset) { console.error('[DbVersioning] No manifest found in release'); return null; } const manifestResponse = await fetch(manifestAsset.browser_download_url); if (!manifestResponse.ok) { console.error('[DbVersioning] Failed to fetch manifest from release'); return null; } return (await manifestResponse.json()) as DbVersionManifest; } catch (error) { console.error('[DbVersioning] Error fetching remote manifest:', error); return null; } } /** * Compare semantic versions * Returns: 1 if remote > local, -1 if local > remote, 0 if equal */ compareVersions(local: string, remote: string): number { const localParts = local.split('.').map((p) => parseInt(p, 10)); const remoteParts = remote.split('.').map((p) => parseInt(p, 10)); for (let i = 0; i < 3; i++) { const l = localParts[i] ?? 0; const r = remoteParts[i] ?? 0; if (r > l) return 1; if (l > r) return -1; } return 0; } /** * Calculate SHA256 hash of file */ async calculateFileHash(filePath: string): Promise<string> { return new Promise((resolve, reject) => { const hash = crypto.createHash('sha256'); const stream = fs.createReadStream(filePath); stream.on('data', (data) => hash.update(data)); stream.on('end', () => resolve(hash.digest('hex'))); stream.on('error', reject); }); } /** * Check if update is available */ async isUpdateAvailable(): Promise<boolean> { try { const local = this.getLocalManifest(); if (!local) { console.info('[DbVersioning] No local manifest found, update available'); return true; } const remote = await this.getRemoteManifest(); if (!remote) { console.info('[DbVersioning] Could not fetch remote manifest'); return false; } const comparison = this.compareVersions(local.version, remote.version); if (comparison < 0) { console.info(`[DbVersioning] Update available: ${local.version} -> ${remote.version}`); return true; } return false; } catch (error) { console.error('[DbVersioning] Error checking for updates:', error); return false; } } /** * Download and verify database file */ async downloadDatabase(manifest: DbVersionManifest): Promise<boolean> { try { console.info(`[DbVersioning] Downloading database version ${manifest.version}...`); const response = await fetch(manifest.downloadUrl); if (!response.ok) { console.error(`[DbVersioning] Failed to download: ${response.status}`); return false; } // Create backup of current database if (fs.existsSync(this.dbPath)) { const backupPath = `${this.dbPath}.backup`; fs.copyFileSync(this.dbPath, backupPath); console.info(`[DbVersioning] Created backup at ${backupPath}`); } // Write downloaded file const buffer = await response.arrayBuffer(); const tempPath = `${this.dbPath}.tmp`; fs.writeFileSync(tempPath, Buffer.from(buffer)); // Verify hash const downloadedHash = await this.calculateFileHash(tempPath); if (downloadedHash !== manifest.hash) { console.error( `[DbVersioning] Hash mismatch: expected ${manifest.hash}, got ${downloadedHash}` ); fs.unlinkSync(tempPath); return false; } // Replace database fs.renameSync(tempPath, this.dbPath); console.info(`[DbVersioning] Successfully updated database to version ${manifest.version}`); return true; } catch (error) { console.error('[DbVersioning] Error downloading database:', error); return false; } } /** * Save manifest locally */ saveManifest(manifest: DbVersionManifest): void { try { if (!fs.existsSync(this.dataDir)) { fs.mkdirSync(this.dataDir, { recursive: true }); } fs.writeFileSync(this.localManifestPath, JSON.stringify(manifest, null, 2)); console.info(`[DbVersioning] Saved manifest version ${manifest.version}`); } catch (error) { console.error('[DbVersioning] Error saving manifest:', error); } } /** * Create new manifest after indexing * Called by build scripts */ async createManifest( version: string, type: 'incremental' | 'full', changelog: string ): Promise<DbVersionManifest> { try { if (!fs.existsSync(this.dbPath)) { throw new Error('Database file not found'); } const hash = await this.calculateFileHash(this.dbPath); const stats = fs.statSync(this.dbPath); const manifest: DbVersionManifest = { version, timestamp: new Date().toISOString(), type, hash, size: stats.size, downloadUrl: `https://github.com/OGMatrix/mcmodding-mcp/releases/download/v${version}/mcmodding-docs.db`, changelog, }; this.saveManifest(manifest); return manifest; } catch (error) { console.error('[DbVersioning] Error creating manifest:', error); throw error; } } /** * Perform automatic update check and download if needed * This is called on MCP startup */ async autoUpdate(): Promise<boolean> { try { const hasUpdate = await this.isUpdateAvailable(); if (!hasUpdate) { console.info('[DbVersioning] Database is up to date'); return false; } const remote = await this.getRemoteManifest(); if (!remote) { console.info('[DbVersioning] Could not fetch remote manifest for update'); return false; } const success = await this.downloadDatabase(remote); if (success) { this.saveManifest(remote); return true; } return false; } catch (error) { console.error('[DbVersioning] Error during auto-update:', error); return false; } } /** * Get version information for display */ getVersionInfo(): { local: string; remote: string | null; upToDate: boolean } { const local = this.getLocalManifest(); return { local: local?.version ?? 'unknown', remote: null, upToDate: true, }; } }

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/OGMatrix/mcmodding-mcp'

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