const express = require('express'); const axios = require('axios'); const app = express(); const port = process.env.PORT || 3000; const GITEA_URL = process.env.GITEA_URL || 'https://git.thedevops.dev'; const GITEA_TOKEN = process.env.GITEA_TOKEN; const GITEA_OWNER = process.env.GITEA_OWNER || 'admin'; if (!GITEA_TOKEN) { console.error('WARNING: GITEA_TOKEN environment variable is not set!'); console.error('Server will start but API calls will fail.'); console.error('Please set GITEA_TOKEN in your .env file'); } const gitea = axios.create({ baseURL: `${GITEA_URL}/api/v1`, headers: { 'Authorization': `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' }, timeout: 30000 }); app.use(express.json()); // CORS middleware app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }); // Health check app.get('/health', async (req, res) => { const health = { status: 'healthy', service: 'mcp-gitea', timestamp: new Date().toISOString(), config: { giteaUrl: GITEA_URL, defaultOwner: GITEA_OWNER, tokenConfigured: !!GITEA_TOKEN } }; // Test connection to Gitea if (GITEA_TOKEN) { try { await gitea.get('/user'); health.giteaConnection = 'ok'; } catch (error) { health.giteaConnection = 'error'; health.giteaError = error.message; } } else { health.giteaConnection = 'not configured'; } res.json(health); }); // Root endpoint app.get('/', (req, res) => { res.json({ service: 'MCP Gitea Server', version: '1.0.0', giteaUrl: GITEA_URL, defaultOwner: GITEA_OWNER, endpoints: { health: 'GET /health', repos: { list: 'POST /api/repos/list', get: 'POST /api/repos/get' }, files: { get: 'POST /api/repos/file/get', create: 'POST /api/repos/file/create', update: 'POST /api/repos/file/update', delete: 'POST /api/repos/file/delete' }, tree: { get: 'POST /api/repos/tree/get' }, branches: { list: 'POST /api/repos/branches/list', create: 'POST /api/repos/branches/create' }, commits: { list: 'POST /api/repos/commits/list' } } }); }); // ============================================================================ // REPOSITORIES // ============================================================================ // List repositories app.post('/api/repos/list', async (req, res) => { try { const owner = req.body.owner || GITEA_OWNER; const response = await gitea.get(`/users/${owner}/repos`); const repos = response.data.map(repo => ({ name: repo.name, fullName: repo.full_name, description: repo.description, private: repo.private, defaultBranch: repo.default_branch, url: repo.html_url, cloneUrl: repo.clone_url, sshUrl: repo.ssh_url, size: repo.size, createdAt: repo.created_at, updatedAt: repo.updated_at })); res.json({ count: repos.length, owner, repos }); } catch (error) { res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // Get repository info app.post('/api/repos/get', async (req, res) => { try { const { owner, repo } = req.body; if (!repo) { return res.status(400).json({ error: 'Repository name is required' }); } const response = await gitea.get(`/repos/${owner || GITEA_OWNER}/${repo}`); res.json(response.data); } catch (error) { res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // ============================================================================ // FILES // ============================================================================ // Get file content app.post('/api/repos/file/get', async (req, res) => { try { const { owner, repo, path, branch } = req.body; if (!repo || !path) { return res.status(400).json({ error: 'Repository name and file path are required' }); } const ref = branch || 'main'; const response = await gitea.get( `/repos/${owner || GITEA_OWNER}/${repo}/contents/${path}`, { params: { ref } } ); const fileData = response.data; // Decode base64 content if present let content = fileData.content; if (fileData.encoding === 'base64') { content = Buffer.from(fileData.content, 'base64').toString('utf-8'); } res.json({ name: fileData.name, path: fileData.path, sha: fileData.sha, size: fileData.size, type: fileData.type, content: content, downloadUrl: fileData.download_url, htmlUrl: fileData.html_url }); } catch (error) { res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // Create file app.post('/api/repos/file/create', async (req, res) => { try { const { owner, repo, path, content, message, branch } = req.body; if (!repo || !path || !content || !message) { return res.status(400).json({ error: 'Repository name, file path, content, and commit message are required' }); } const data = { content: Buffer.from(content).toString('base64'), message: message, branch: branch || 'main' }; const response = await gitea.post( `/repos/${owner || GITEA_OWNER}/${repo}/contents/${path}`, data ); res.json({ message: 'File created successfully', file: path, sha: response.data.content?.sha, commit: response.data.commit }); } catch (error) { res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // Update file app.post('/api/repos/file/update', async (req, res) => { try { const { owner, repo, path, content, message, sha, branch } = req.body; if (!repo || !path || !content || !message || !sha) { return res.status(400).json({ error: 'Repository name, file path, content, commit message, and SHA are required' }); } const data = { content: Buffer.from(content).toString('base64'), message: message, sha: sha, branch: branch || 'main' }; const response = await gitea.put( `/repos/${owner || GITEA_OWNER}/${repo}/contents/${path}`, data ); res.json({ message: 'File updated successfully', file: path, sha: response.data.content?.sha, commit: response.data.commit }); } catch (error) { res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // Delete file app.post('/api/repos/file/delete', async (req, res) => { try { const { owner, repo, path, message, sha, branch } = req.body; if (!repo || !path || !message || !sha) { return res.status(400).json({ error: 'Repository name, file path, commit message, and SHA are required' }); } const data = { message: message, sha: sha, branch: branch || 'main' }; const response = await gitea.delete( `/repos/${owner || GITEA_OWNER}/${repo}/contents/${path}`, { data } ); res.json({ message: 'File deleted successfully', file: path, commit: response.data.commit }); } catch (error) { res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // ============================================================================ // DIRECTORY TREE // ============================================================================ // Get directory contents app.post('/api/repos/tree/get', async (req, res) => { try { const { owner, repo, path, branch } = req.body; if (!repo) { return res.status(400).json({ error: 'Repository name is required' }); } const ref = branch || 'main'; const treePath = path || ''; const response = await gitea.get( `/repos/${owner || GITEA_OWNER}/${repo}/contents/${treePath}`, { params: { ref } } ); const entries = Array.isArray(response.data) ? response.data : [response.data]; const tree = entries.map(entry => ({ name: entry.name, path: entry.path, type: entry.type, size: entry.size, sha: entry.sha, downloadUrl: entry.download_url, htmlUrl: entry.html_url })); res.json({ path: treePath || '/', branch: ref, count: tree.length, entries: tree }); } catch (error) { res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // ============================================================================ // BRANCHES // ============================================================================ // List branches app.post('/api/repos/branches/list', async (req, res) => { try { const { owner, repo } = req.body; if (!repo) { return res.status(400).json({ error: 'Repository name is required' }); } const response = await gitea.get( `/repos/${owner || GITEA_OWNER}/${repo}/branches` ); const branches = response.data.map(branch => ({ name: branch.name, commit: { sha: branch.commit.id, message: branch.commit.message, author: branch.commit.author.name, date: branch.commit.timestamp }, protected: branch.protected })); res.json({ count: branches.length, branches }); } catch (error) { res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // Create branch app.post('/api/repos/branches/create', async (req, res) => { try { const { owner, repo, branch, from } = req.body; if (!repo || !branch) { return res.status(400).json({ error: 'Repository name and new branch name are required' }); } const data = { new_branch_name: branch, old_branch_name: from || 'main' }; const response = await gitea.post( `/repos/${owner || GITEA_OWNER}/${repo}/branches`, data ); res.json({ message: 'Branch created successfully', branch: branch, from: from || 'main' }); } catch (error) { res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // ============================================================================ // COMMITS // ============================================================================ // List commits app.post('/api/repos/commits/list', async (req, res) => { try { const { owner, repo, branch, path, limit } = req.body; if (!repo) { return res.status(400).json({ error: 'Repository name is required' }); } const params = { sha: branch || 'main', path: path || undefined, limit: limit || 10 }; const response = await gitea.get( `/repos/${owner || GITEA_OWNER}/${repo}/commits`, { params } ); const commits = response.data.map(commit => ({ sha: commit.sha, message: commit.commit.message, author: { name: commit.commit.author.name, email: commit.commit.author.email, date: commit.commit.author.date }, committer: { name: commit.commit.committer.name, date: commit.commit.committer.date }, htmlUrl: commit.html_url })); res.json({ count: commits.length, commits }); } catch (error) { res.status(error.response?.status || 500).json({ error: error.message, details: error.response?.data }); } }); // ============================================================================ // ERROR HANDLING // ============================================================================ // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Endpoint not found', message: `${req.method} ${req.path} is not a valid endpoint`, availableEndpoints: 'GET / for list of available endpoints' }); }); // Error handler app.use((err, req, res, next) => { console.error('Error:', err); res.status(500).json({ error: 'Internal server error', message: err.message }); }); // ============================================================================ // START SERVER // ============================================================================ app.listen(port, '0.0.0.0', () => { console.log(`╔═══════════════════════════════════════════════════════╗`); console.log(`║ ║`); console.log(`║ MCP Gitea Server ║`); console.log(`║ Running on port ${port} ║`); console.log(`║ ║`); console.log(`╚═══════════════════════════════════════════════════════╝`); console.log(`\nGitea URL: ${GITEA_URL}`); console.log(`Default owner: ${GITEA_OWNER}`); console.log(`Token configured: ${GITEA_TOKEN ? 'Yes' : 'No'}`); console.log(`\nEndpoints available at http://0.0.0.0:${port}`); console.log(`Health check: http://0.0.0.0:${port}/health`); });