feat: initial implementation of VirtFusion MCP server
Complete MCP server wrapping all 84 VirtFusion Admin API endpoints: - Core HTTP client with Bearer auth and error handling - 17 tool modules organized by API category - Endpoint drift detection scripts and Gitea Actions CI/CD - Comprehensive README with configuration examples - CLAUDE.md for AI assistant onboarding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
79
scripts/check-endpoint-drift.ts
Normal file
79
scripts/check-endpoint-drift.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { parse } from 'yaml';
|
||||
|
||||
interface Endpoint {
|
||||
method: string;
|
||||
path: string;
|
||||
summary: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
function extractEndpoints(specPath: string): Endpoint[] {
|
||||
const spec = parse(readFileSync(specPath, 'utf-8'));
|
||||
const endpoints: Endpoint[] = [];
|
||||
|
||||
for (const [path, methods] of Object.entries(spec.paths as Record<string, Record<string, unknown>>)) {
|
||||
for (const [method, details] of Object.entries(methods)) {
|
||||
if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) {
|
||||
const op = details as { summary?: string; tags?: string[] };
|
||||
endpoints.push({
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
summary: op.summary ?? '',
|
||||
tag: op.tags?.[0] ?? 'Untagged',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
||||
}
|
||||
|
||||
function endpointKey(e: Endpoint): string {
|
||||
return `${e.method} ${e.path}`;
|
||||
}
|
||||
|
||||
const specPath = new URL('../openapi.yaml', import.meta.url).pathname;
|
||||
const manifestPath = new URL('../endpoint-manifest.json', import.meta.url).pathname;
|
||||
|
||||
const current = extractEndpoints(specPath);
|
||||
|
||||
let manifest: Endpoint[];
|
||||
try {
|
||||
manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
||||
} catch {
|
||||
console.error('Error: endpoint-manifest.json not found. Run `npm run extract-endpoints > endpoint-manifest.json` first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const manifestKeys = new Set(manifest.map(endpointKey));
|
||||
const currentKeys = new Set(current.map(endpointKey));
|
||||
|
||||
const added = current.filter((e) => !manifestKeys.has(endpointKey(e)));
|
||||
const removed = manifest.filter((e) => !currentKeys.has(endpointKey(e)));
|
||||
|
||||
if (added.length === 0 && removed.length === 0) {
|
||||
console.log(`No endpoint drift detected. ${current.length} endpoints match the manifest.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('Endpoint drift detected!\n');
|
||||
|
||||
if (added.length > 0) {
|
||||
console.log(`New endpoints (${added.length}):`);
|
||||
for (const e of added) {
|
||||
console.log(` + ${e.method} ${e.path} — ${e.summary} [${e.tag}]`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (removed.length > 0) {
|
||||
console.log(`Removed endpoints (${removed.length}):`);
|
||||
for (const e of removed) {
|
||||
console.log(` - ${e.method} ${e.path} — ${e.summary} [${e.tag}]`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log('Update endpoint-manifest.json: npm run extract-endpoints > endpoint-manifest.json');
|
||||
process.exit(1);
|
||||
32
scripts/extract-endpoints.ts
Normal file
32
scripts/extract-endpoints.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { parse } from 'yaml';
|
||||
|
||||
interface Endpoint {
|
||||
method: string;
|
||||
path: string;
|
||||
summary: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
const specPath = new URL('../openapi.yaml', import.meta.url).pathname;
|
||||
const spec = parse(readFileSync(specPath, 'utf-8'));
|
||||
|
||||
const endpoints: Endpoint[] = [];
|
||||
|
||||
for (const [path, methods] of Object.entries(spec.paths as Record<string, Record<string, unknown>>)) {
|
||||
for (const [method, details] of Object.entries(methods)) {
|
||||
if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) {
|
||||
const op = details as { summary?: string; tags?: string[] };
|
||||
endpoints.push({
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
summary: op.summary ?? '',
|
||||
tag: op.tags?.[0] ?? 'Untagged',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endpoints.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
||||
|
||||
console.log(JSON.stringify(endpoints, null, 2));
|
||||
Reference in New Issue
Block a user