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