feat: add index.js for mcp-gitea server
This commit is contained in:
510
apps/ollama-mcp/mcp-gitea/index.js
Normal file
510
apps/ollama-mcp/mcp-gitea/index.js
Normal 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`);
|
||||
});
|
||||
Reference in New Issue
Block a user