/**
* Gitea Provider
*
* Implementation of BaseProvider for Gitea API operations.
* Handles authentication, repository management, issues, PRs, and more.
*/
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { BaseProvider } from './base-provider.js';
import { ProviderResult } from './types.js';
import { retry } from '../utils/retry.js';
export interface GiteaConfig {
url: string;
token: string;
username: string;
}
export class GiteaProvider extends BaseProvider {
private client: AxiosInstance | null = null;
private giteaConfig: GiteaConfig;
constructor(config: GiteaConfig) {
super('gitea', config);
this.giteaConfig = config;
if (this.isConfigured()) {
this.client = axios.create({
baseURL: `${this.giteaConfig.url}/api/v1`,
headers: {
'Authorization': `token ${this.giteaConfig.token}`,
'Content-Type': 'application/json',
'User-Agent': 'git-mcp/1.0.0'
},
timeout: 30000
});
}
}
/**
* Check if Gitea provider is properly configured
*/
isConfigured(): boolean {
return !!(this.giteaConfig.url && this.giteaConfig.token && this.giteaConfig.username);
}
/**
* Validate Gitea credentials by making a test API call
*/
async validateCredentials(): Promise<boolean> {
if (!this.isConfigured() || !this.client) {
return false;
}
try {
await this.client.get('/user');
return true;
} catch (error) {
return false;
}
}
/**
* Execute a Gitea operation
*/
async executeOperation(operation: string, params: any): Promise<ProviderResult> {
if (!this.isConfigured()) {
return this.formatError(
'GITEA_NOT_CONFIGURED',
'Gitea provider is not configured. Please set GITEA_URL, GITEA_TOKEN and GITEA_USERNAME environment variables.',
{ missingFields: this.getMissingConfigFields() }
);
}
if (!this.client) {
return this.formatError(
'GITEA_CLIENT_ERROR',
'Gitea client is not initialized'
);
}
if (!this.isOperationSupported(operation)) {
return this.formatError(
'UNSUPPORTED_OPERATION',
`Operation '${operation}' is not supported by Gitea provider`,
{ supportedOperations: this.getSupportedOperations() }
);
}
try {
const result = await retry(() => this.executeGiteaOperation(operation, params), {
retries: 3,
factor: 2,
minTimeout: 200,
maxTimeout: 2000,
retryOn: (err) => {
if (!err) return false;
const status = err.status || err.statusCode || (err.response && err.response.status);
if (!status) return true;
return status >= 500 || status === 429;
}
});
return this.formatSuccess(result);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return this.formatError(
'GITEA_API_ERROR',
`Gitea API error: ${errorMessage}`,
error
);
}
}
/**
* Get supported operations for Gitea provider
*/
getSupportedOperations(): string[] {
return [
// Repository operations
'repo-create', 'repo-list', 'repo-get', 'repo-update', 'repo-delete', 'repo-fork', 'repo-search',
// Issue operations
'issue-create', 'issue-list', 'issue-get', 'issue-update', 'issue-close', 'issue-comment', 'issue-search',
// Pull request operations
'pr-create', 'pr-list', 'pr-get', 'pr-update', 'pr-merge', 'pr-close', 'pr-review', 'pr-search',
// Branch operations
'branch-create', 'branch-list', 'branch-get', 'branch-delete', 'branch-compare',
// Tag operations
'tag-create', 'tag-list', 'tag-get', 'tag-delete', 'tag-search',
// Release operations
'release-create', 'release-list', 'release-get', 'release-update', 'release-delete', 'release-publish', 'release-download',
// File operations (read-only only)
'file-read', 'file-search',
// Backwards/IDE compatibility aliases
'listFiles', 'getFile',
// Package operations (limited support in Gitea)
'package-list', 'package-get', 'package-delete'
];
}
/**
* Get missing configuration fields
*/
protected getMissingConfigFields(): string[] {
const missing: string[] = [];
if (!this.giteaConfig.url) {
missing.push('GITEA_URL');
}
if (!this.giteaConfig.token) {
missing.push('GITEA_TOKEN');
}
if (!this.giteaConfig.username) {
missing.push('GITEA_USERNAME');
}
return missing;
}
/**
* Execute specific Gitea operations
*/
private async executeGiteaOperation(operation: string, params: any): Promise<any> {
if (!this.client) {
throw new Error('Gitea client not initialized');
}
// Repository operations
if (operation.startsWith('repo-')) {
return this.executeRepositoryOperation(operation, params);
}
// Issue operations
if (operation.startsWith('issue-')) {
return this.executeIssueOperation(operation, params);
}
// Pull request operations
if (operation.startsWith('pr-')) {
return this.executePullRequestOperation(operation, params);
}
// Branch operations
if (operation.startsWith('branch-')) {
return this.executeBranchOperation(operation, params);
}
// Tag operations
if (operation.startsWith('tag-')) {
return this.executeTagOperation(operation, params);
}
// Release operations
if (operation.startsWith('release-')) {
return this.executeReleaseOperation(operation, params);
}
// File operations (including compatibility aliases)
if (operation.startsWith('file-') || operation === 'listFiles' || operation === 'getFile') {
return this.executeFileOperation(operation, params);
}
// Package operations
if (operation.startsWith('package-')) {
return this.executePackageOperation(operation, params);
}
throw new Error(`Unsupported operation: ${operation}`);
}
/**
* Execute repository operations
*/
private async executeRepositoryOperation(operation: string, params: any): Promise<any> {
const { owner = this.giteaConfig.username, repo, ...otherParams } = params;
switch (operation) {
case 'repo-create':
return this.makeRequest('POST', '/user/repos', {
name: params.name,
description: params.description,
private: params.private || false,
...otherParams
});
case 'repo-list':
return this.makeRequest('GET', `/users/${owner}/repos`, {
params: {
type: params.type || 'all',
...otherParams
}
});
case 'repo-get':
return this.makeRequest('GET', `/repos/${owner}/${repo}`);
case 'repo-update':
return this.makeRequest('PATCH', `/repos/${owner}/${repo}`, otherParams);
case 'repo-delete':
return this.makeRequest('DELETE', `/repos/${owner}/${repo}`);
case 'repo-fork':
return this.makeRequest('POST', `/repos/${owner}/${repo}/forks`, {
organization: params.organization,
...otherParams
});
case 'repo-search':
return this.makeRequest('GET', '/repos/search', {
params: {
q: params.query,
sort: params.sort,
order: params.order,
...otherParams
}
});
default:
throw new Error(`Unsupported repository operation: ${operation}`);
}
}
/**
* Execute issue operations
*/
private async executeIssueOperation(operation: string, params: any): Promise<any> {
const { owner = this.giteaConfig.username, repo, issue_number, ...otherParams } = params;
switch (operation) {
case 'issue-create':
return this.makeRequest('POST', `/repos/${owner}/${repo}/issues`, {
title: params.title,
body: params.body,
labels: params.labels,
...otherParams
});
case 'issue-list':
return this.makeRequest('GET', `/repos/${owner}/${repo}/issues`, {
params: {
state: params.state || 'open',
...otherParams
}
});
case 'issue-get':
return this.makeRequest('GET', `/repos/${owner}/${repo}/issues/${issue_number}`);
case 'issue-update':
return this.makeRequest('PATCH', `/repos/${owner}/${repo}/issues/${issue_number}`, otherParams);
case 'issue-close':
return this.makeRequest('PATCH', `/repos/${owner}/${repo}/issues/${issue_number}`, {
state: 'closed',
...otherParams
});
case 'issue-comment':
return this.makeRequest('POST', `/repos/${owner}/${repo}/issues/${issue_number}/comments`, {
body: params.body,
...otherParams
});
case 'issue-search':
return this.makeRequest('GET', '/repos/issues/search', {
params: {
q: params.query,
sort: params.sort,
order: params.order,
...otherParams
}
});
default:
throw new Error(`Unsupported issue operation: ${operation}`);
}
}
/**
* Execute pull request operations
*/
private async executePullRequestOperation(operation: string, params: any): Promise<any> {
const { owner = this.giteaConfig.username, repo, pull_number, ...otherParams } = params;
switch (operation) {
case 'pr-create':
return this.makeRequest('POST', `/repos/${owner}/${repo}/pulls`, {
title: params.title,
head: params.head,
base: params.base,
body: params.body,
...otherParams
});
case 'pr-list':
return this.makeRequest('GET', `/repos/${owner}/${repo}/pulls`, {
params: {
state: params.state || 'open',
...otherParams
}
});
case 'pr-get':
return this.makeRequest('GET', `/repos/${owner}/${repo}/pulls/${pull_number}`);
case 'pr-update':
return this.makeRequest('PATCH', `/repos/${owner}/${repo}/pulls/${pull_number}`, otherParams);
case 'pr-merge':
return this.makeRequest('POST', `/repos/${owner}/${repo}/pulls/${pull_number}/merge`, {
Do: params.merge_method || 'merge',
MergeTitleField: params.commit_title,
MergeMessageField: params.commit_message,
...otherParams
});
case 'pr-close':
return this.makeRequest('PATCH', `/repos/${owner}/${repo}/pulls/${pull_number}`, {
state: 'closed',
...otherParams
});
case 'pr-review':
return this.makeRequest('POST', `/repos/${owner}/${repo}/pulls/${pull_number}/reviews`, {
event: params.event,
body: params.body,
...otherParams
});
case 'pr-search':
return this.makeRequest('GET', '/repos/issues/search', {
params: {
q: `${params.query} type:pr`,
sort: params.sort,
order: params.order,
...otherParams
}
});
default:
throw new Error(`Unsupported pull request operation: ${operation}`);
}
}
/**
* Execute branch operations
*/
private async executeBranchOperation(operation: string, params: any): Promise<any> {
const { owner = this.giteaConfig.username, repo, ...otherParams } = params;
switch (operation) {
case 'branch-create':
return this.makeRequest('POST', `/repos/${owner}/${repo}/branches`, {
new_branch_name: params.branch,
old_branch_name: params.from_branch || 'main',
...otherParams
});
case 'branch-list':
return this.makeRequest('GET', `/repos/${owner}/${repo}/branches`);
case 'branch-get':
return this.makeRequest('GET', `/repos/${owner}/${repo}/branches/${params.branch}`);
case 'branch-delete':
return this.makeRequest('DELETE', `/repos/${owner}/${repo}/branches/${params.branch}`);
case 'branch-compare':
return this.makeRequest('GET', `/repos/${owner}/${repo}/compare/${params.base}...${params.head}`);
default:
throw new Error(`Unsupported branch operation: ${operation}`);
}
}
/**
* Execute tag operations
*/
private async executeTagOperation(operation: string, params: any): Promise<any> {
const { owner = this.giteaConfig.username, repo, ...otherParams } = params;
switch (operation) {
case 'tag-create':
return this.makeRequest('POST', `/repos/${owner}/${repo}/tags`, {
tag_name: params.tag,
message: params.message,
target: params.object,
...otherParams
});
case 'tag-list':
return this.makeRequest('GET', `/repos/${owner}/${repo}/tags`);
case 'tag-get':
return this.makeRequest('GET', `/repos/${owner}/${repo}/git/tags/${params.tag_sha}`);
case 'tag-delete':
return this.makeRequest('DELETE', `/repos/${owner}/${repo}/tags/${params.tag}`);
case 'tag-search':
return this.makeRequest('GET', `/repos/${owner}/${repo}/tags`, {
params: otherParams
});
default:
throw new Error(`Unsupported tag operation: ${operation}`);
}
}
/**
* Execute release operations
*/
private async executeReleaseOperation(operation: string, params: any): Promise<any> {
const { owner = this.giteaConfig.username, repo, release_id, ...otherParams } = params;
switch (operation) {
case 'release-create':
return this.makeRequest('POST', `/repos/${owner}/${repo}/releases`, {
tag_name: params.tag_name,
name: params.name,
body: params.body,
draft: params.draft || false,
prerelease: params.prerelease || false,
...otherParams
});
case 'release-list':
return this.makeRequest('GET', `/repos/${owner}/${repo}/releases`);
case 'release-get':
// If release_id is not provided, get it from tagName
let getReleaseId = release_id;
if (!getReleaseId && params.tagName) {
const releases = await this.makeRequest('GET', `/repos/${owner}/${repo}/releases`);
const release = releases.find((r: any) => r.tag_name === params.tagName);
if (!release) {
throw new Error(`Release with tag '${params.tagName}' not found`);
}
getReleaseId = release.id;
}
return this.makeRequest('GET', `/repos/${owner}/${repo}/releases/${getReleaseId}`);
case 'release-update':
// If release_id is not provided, get it from tagName
let updateReleaseId = release_id;
if (!updateReleaseId && params.tagName) {
const releases = await this.makeRequest('GET', `/repos/${owner}/${repo}/releases`);
const release = releases.find((r: any) => r.tag_name === params.tagName);
if (!release) {
throw new Error(`Release with tag '${params.tagName}' not found`);
}
updateReleaseId = release.id;
}
return this.makeRequest('PATCH', `/repos/${owner}/${repo}/releases/${updateReleaseId}`, otherParams);
case 'release-delete':
// If release_id is not provided, get it from tagName
let deleteReleaseId = release_id;
if (!deleteReleaseId && params.tagName) {
const releases = await this.makeRequest('GET', `/repos/${owner}/${repo}/releases`);
const release = releases.find((r: any) => r.tag_name === params.tagName);
if (!release) {
throw new Error(`Release with tag '${params.tagName}' not found`);
}
deleteReleaseId = release.id;
}
return this.makeRequest('DELETE', `/repos/${owner}/${repo}/releases/${deleteReleaseId}`);
case 'release-publish':
// If release_id is not provided, get it from tagName
let publishReleaseId = release_id;
if (!publishReleaseId && params.tagName) {
const releases = await this.makeRequest('GET', `/repos/${owner}/${repo}/releases`);
const release = releases.find((r: any) => r.tag_name === params.tagName);
if (!release) {
throw new Error(`Release with tag '${params.tagName}' not found`);
}
publishReleaseId = release.id;
}
return this.makeRequest('PATCH', `/repos/${owner}/${repo}/releases/${publishReleaseId}`, {
draft: false,
...otherParams
});
case 'release-download':
return this.makeRequest('GET', `/repos/${owner}/${repo}/releases/${release_id}/assets/${params.asset_id}`);
default:
throw new Error(`Unsupported release operation: ${operation}`);
}
}
/**
* Execute file operations
*/
private async executeFileOperation(operation: string, params: any): Promise<any> {
const { owner = this.giteaConfig.username, repo, path, ...otherParams } = params;
switch (operation) {
case 'file-read':
return this.makeRequest('GET', `/repos/${owner}/${repo}/contents/${path}`, {
params: otherParams
});
case 'listFiles': {
const listPath = path || '';
const response = await this.makeRequest('GET', `/repos/${owner}/${repo}/contents/${listPath}`, {
params: otherParams
});
// Gitea returns array for directories
const data = Array.isArray(response)
? response.map((item: any) => ({
name: item.name,
path: item.path,
sha: item.sha,
type: item.type,
size: item.size,
url: item.html_url || item.url
}))
: ({
name: response.name,
path: response.path,
sha: response.sha,
type: response.type,
size: response.size,
url: response.html_url || response.url
});
return data;
}
case 'getFile': {
const res = await this.makeRequest('GET', `/repos/${owner}/${repo}/contents/${path}`, {
params: otherParams
});
const fileData: any = Array.isArray(res) ? res[0] : res;
const contentEncoded = fileData.content;
let contentDecoded: string | null = null;
if (contentEncoded) {
try {
contentDecoded = Buffer.from(contentEncoded, 'base64').toString('utf8');
} catch (e) {
contentDecoded = null;
}
}
return {
filePath: fileData.path,
sha: fileData.sha,
encoding: fileData.encoding || (contentEncoded ? 'base64' : undefined),
content: contentDecoded,
size: fileData.size,
url: fileData.html_url || fileData.url
};
}
case 'file-create':
case 'file-update':
case 'file-delete':
throw new Error(`File modification operations (${operation}) are not allowed. This provider only supports read-only file operations for security reasons.`);
case 'file-search':
return this.makeRequest('GET', `/repos/${owner}/${repo}/search`, {
params: {
q: params.query,
...otherParams
}
});
default:
throw new Error(`Unsupported file operation: ${operation}`);
}
}
/**
* Execute package operations (limited support in Gitea)
*/
private async executePackageOperation(operation: string, params: any): Promise<any> {
const { owner = this.giteaConfig.username, ...otherParams } = params;
switch (operation) {
case 'package-list':
return this.makeRequest('GET', `/packages/${owner}`, {
params: {
type: params.package_type || 'npm',
...otherParams
}
});
case 'package-get':
return this.makeRequest('GET', `/packages/${owner}/${params.package_type || 'npm'}/${params.package_name}`);
case 'package-delete':
return this.makeRequest('DELETE', `/packages/${owner}/${params.package_type || 'npm'}/${params.package_name}`);
case 'package-create':
case 'package-update':
case 'package-publish':
throw new Error('Package creation/update/publishing should be done through package managers');
case 'package-download':
return this.makeRequest('GET', `/packages/${owner}/${params.package_type || 'npm'}/${params.package_name}/${params.version}/files`);
default:
throw new Error(`Unsupported package operation: ${operation}`);
}
}
/**
* Make HTTP request to Gitea API
*/
private async makeRequest(method: string, endpoint: string, data?: any): Promise<any> {
if (!this.client) {
throw new Error('Gitea client not initialized');
}
let response: AxiosResponse;
switch (method.toLowerCase()) {
case 'get':
response = await this.client.get(endpoint, data);
break;
case 'post':
response = await this.client.post(endpoint, data);
break;
case 'put':
response = await this.client.put(endpoint, data);
break;
case 'patch':
response = await this.client.patch(endpoint, data);
break;
case 'delete':
response = await this.client.delete(endpoint, data);
break;
default:
throw new Error(`Unsupported HTTP method: ${method}`);
}
return response.data;
}
}