Skip to main content
Glama
gitea-provider.ts21.7 kB
/** * 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; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Andre-Buzeli/git-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server