feat: add index.js for mcp-gitea server

This commit is contained in:
Claude AI
2026-01-11 13:19:49 +00:00
parent c01490b5a0
commit fc28f054a2

View File

@@ -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`);
});