const express = require('express'); const axios = require('axios'); const https = require('https'); const app = express(); const port = process.env.PORT || 3000; const ARGOCD_URL = (process.env.ARGOCD_URL || 'https://argocd.thedevops.dev').replace(/\/$/, ''); const ARGOCD_TOKEN = process.env.ARGOCD_TOKEN || ''; // Allow self-signed certs in homelab const httpsAgent = new https.Agent({ rejectUnauthorized: false }); const argoApi = axios.create({ baseURL: `${ARGOCD_URL}/api/v1`, httpsAgent, headers: ARGOCD_TOKEN ? { Authorization: `Bearer ${ARGOCD_TOKEN}` } : {} }); app.use(express.json()); app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') return res.sendStatus(200); next(); }); app.get('/health', (req, res) => res.json({ status: 'healthy', service: 'mcp-argocd' })); app.get('/', (req, res) => res.json({ service: 'MCP ArgoCD Server', version: '1.0.0', argocd: ARGOCD_URL, endpoints: [ 'POST /api/apps/list - List all applications with sync/health status', 'POST /api/apps/get - Get detailed app info (name required)', 'POST /api/apps/sync - Trigger sync for an app (name required)', 'POST /api/apps/out_of_sync - List all out-of-sync applications', 'POST /api/apps/unhealthy - List all unhealthy applications', 'POST /api/apps/history - Get deployment history for an app', 'POST /api/apps/rollback - Rollback app to previous revision', 'POST /api/clusters/list - List connected clusters', 'POST /api/repos/list - List connected repositories', ] })); // List all apps app.post('/api/apps/list', async (req, res) => { try { const r = await argoApi.get('/applications'); const apps = (r.data.items || []).map(a => ({ name: a.metadata.name, namespace: a.spec.destination?.namespace, project: a.spec.project, syncStatus: a.status?.sync?.status, healthStatus: a.status?.health?.status, repo: a.spec.source?.repoURL, path: a.spec.source?.path, targetRevision: a.spec.source?.targetRevision, lastSyncedAt: a.status?.operationState?.finishedAt, })); res.json({ count: apps.length, apps }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Get app details app.post('/api/apps/get', async (req, res) => { try { const { name } = req.body; if (!name) return res.status(400).json({ error: 'name is required' }); const r = await argoApi.get(`/applications/${name}`); const a = r.data; res.json({ name: a.metadata.name, project: a.spec.project, source: a.spec.source, destination: a.spec.destination, syncPolicy: a.spec.syncPolicy, syncStatus: a.status?.sync, healthStatus: a.status?.health, conditions: a.status?.conditions, resources: (a.status?.resources || []).map(r => ({ kind: r.kind, name: r.name, namespace: r.namespace, status: r.status, health: r.health?.status })) }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Sync app app.post('/api/apps/sync', async (req, res) => { try { const { name, prune, force } = req.body; if (!name) return res.status(400).json({ error: 'name is required' }); const body = { prune: prune || false }; if (force) body.strategy = { apply: { force: true } }; const r = await argoApi.post(`/applications/${name}/sync`, body); res.json({ message: `Sync triggered for ${name}`, phase: r.data.status?.operationState?.phase, startedAt: r.data.status?.operationState?.startedAt }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Out-of-sync apps app.post('/api/apps/out_of_sync', async (req, res) => { try { const r = await argoApi.get('/applications'); const apps = (r.data.items || []) .filter(a => a.status?.sync?.status !== 'Synced') .map(a => ({ name: a.metadata.name, syncStatus: a.status?.sync?.status, healthStatus: a.status?.health?.status, namespace: a.spec.destination?.namespace, lastSyncedAt: a.status?.operationState?.finishedAt })); res.json({ count: apps.length, apps }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Unhealthy apps app.post('/api/apps/unhealthy', async (req, res) => { try { const r = await argoApi.get('/applications'); const apps = (r.data.items || []) .filter(a => !['Healthy', 'Progressing'].includes(a.status?.health?.status)) .map(a => ({ name: a.metadata.name, healthStatus: a.status?.health?.status, healthMessage: a.status?.health?.message, syncStatus: a.status?.sync?.status, namespace: a.spec.destination?.namespace, conditions: a.status?.conditions })); res.json({ count: apps.length, apps }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Deployment history app.post('/api/apps/history', async (req, res) => { try { const { name, limit } = req.body; if (!name) return res.status(400).json({ error: 'name is required' }); const r = await argoApi.get(`/applications/${name}/revisions`); const history = ((r.data.history || []).slice(-(limit || 10))).reverse().map(h => ({ id: h.id, revision: h.revision, deployedAt: h.deployedAt, initiatedBy: h.initiatedBy })); res.json({ app: name, count: history.length, history }); } catch (e) { // history endpoint may differ by ArgoCD version try { const r2 = await argoApi.get(`/applications/${name}`); const history = ((