From fc28f054a2b3c5d2377930c41a8efd54f4e8d29f Mon Sep 17 00:00:00 2001 From: Claude AI Date: Sun, 11 Jan 2026 13:19:49 +0000 Subject: [PATCH] feat: add index.js for mcp-gitea server --- apps/ollama-mcp/mcp-gitea/index.js | 510 +++++++++++++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 apps/ollama-mcp/mcp-gitea/index.js diff --git a/apps/ollama-mcp/mcp-gitea/index.js b/apps/ollama-mcp/mcp-gitea/index.js new file mode 100644 index 0000000..2b8e56e --- /dev/null +++ b/apps/ollama-mcp/mcp-gitea/index.js @@ -0,0 +1,510 @@ +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`); +});