Skip to main content
Glama
github-provider.ts22.9 kB
/** * GitHub Provider * * Implementation of BaseProvider for GitHub API operations. * Handles authentication, repository management, issues, PRs, and more. */ import { Octokit } from '@octokit/rest'; import { BaseProvider } from './base-provider.js'; import { retry } from '../utils/retry.js'; import { ProviderResult } from './types.js'; export interface GitHubConfig { token: string; username: string; } export class GitHubProvider extends BaseProvider { private octokit: Octokit | null = null; private githubConfig: GitHubConfig; constructor(config: GitHubConfig) { super('github', config); this.githubConfig = config; if (this.isConfigured()) { this.octokit = new Octokit({ auth: this.githubConfig.token, userAgent: 'git-mcp/1.0.0' }); } } /** * Check if GitHub provider is properly configured */ isConfigured(): boolean { return !!(this.githubConfig.token && this.githubConfig.username); } /** * Validate GitHub credentials by making a test API call */ async validateCredentials(): Promise<boolean> { if (!this.isConfigured() || !this.octokit) { return false; } try { await this.octokit.rest.users.getAuthenticated(); return true; } catch (error) { return false; } } /** * Execute a GitHub operation */ async executeOperation(operation: string, params: any): Promise<ProviderResult> { if (!this.isConfigured()) { return this.formatError( 'GITHUB_NOT_CONFIGURED', 'GitHub provider is not configured. Please set GITHUB_TOKEN and GITHUB_USERNAME environment variables.', { missingFields: this.getMissingConfigFields() } ); } if (!this.octokit) { return this.formatError( 'GITHUB_CLIENT_ERROR', 'GitHub client is not initialized' ); } if (!this.isOperationSupported(operation)) { return this.formatError( 'UNSUPPORTED_OPERATION', `Operation '${operation}' is not supported by GitHub provider`, { supportedOperations: this.getSupportedOperations() } ); } try { const result = await retry(() => this.executeGitHubOperation(operation, params), { retries: 3, factor: 2, minTimeout: 200, maxTimeout: 2000, retryOn: (err) => { // Retry on network errors and 5xx responses if (!err) return false; const status = err.status || err.statusCode || (err.response && err.response.status); if (!status) return true; // network or unknown error return status >= 500 || status === 429; } }); return this.formatSuccess(result); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return this.formatError( 'GITHUB_API_ERROR', `GitHub API error: ${errorMessage}`, error ); } } /** * Get supported operations for GitHub 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 used by some MCP clients 'listFiles', 'getFile', // Package operations 'package-list', 'package-get', 'package-create', 'package-update', 'package-delete', 'package-publish', 'package-download' ]; } /** * Get missing configuration fields */ protected getMissingConfigFields(): string[] { const missing: string[] = []; if (!this.githubConfig.token) { missing.push('GITHUB_TOKEN'); } if (!this.githubConfig.username) { missing.push('GITHUB_USERNAME'); } return missing; } /** * Execute specific GitHub operations */ private async executeGitHubOperation(operation: string, params: any): Promise<any> { if (!this.octokit) { throw new Error('GitHub 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.githubConfig.username, repo, ...otherParams } = params; switch (operation) { case 'repo-create': return this.octokit!.rest.repos.createForAuthenticatedUser({ name: params.name, description: params.description, private: params.private || false, ...otherParams }); case 'repo-list': return this.octokit!.rest.repos.listForUser({ username: owner, type: params.type || 'all', ...otherParams }); case 'repo-get': return this.octokit!.rest.repos.get({ owner, repo, ...otherParams }); case 'repo-update': return this.octokit!.rest.repos.update({ owner, repo, ...otherParams }); case 'repo-delete': return this.octokit!.rest.repos.delete({ owner, repo, ...otherParams }); case 'repo-fork': return this.octokit!.rest.repos.createFork({ owner, repo, organization: params.organization, ...otherParams }); case 'repo-search': return this.octokit!.rest.search.repos({ 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.githubConfig.username, repo, issue_number, ...otherParams } = params; switch (operation) { case 'issue-create': return this.octokit!.rest.issues.create({ owner, repo, title: params.title, body: params.body, labels: params.labels, ...otherParams }); case 'issue-list': return this.octokit!.rest.issues.listForRepo({ owner, repo, state: params.state || 'open', ...otherParams }); case 'issue-get': return this.octokit!.rest.issues.get({ owner, repo, issue_number, ...otherParams }); case 'issue-update': return this.octokit!.rest.issues.update({ owner, repo, issue_number, ...otherParams }); case 'issue-close': return this.octokit!.rest.issues.update({ owner, repo, issue_number, state: 'closed', ...otherParams }); case 'issue-comment': return this.octokit!.rest.issues.createComment({ owner, repo, issue_number, body: params.body, ...otherParams }); case 'issue-search': return this.octokit!.rest.search.issuesAndPullRequests({ q: `${params.query} type:issue`, 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.githubConfig.username, repo, pull_number, ...otherParams } = params; switch (operation) { case 'pr-create': return this.octokit!.rest.pulls.create({ owner, repo, title: params.title, head: params.head, base: params.base, body: params.body, ...otherParams }); case 'pr-list': return this.octokit!.rest.pulls.list({ owner, repo, state: params.state || 'open', ...otherParams }); case 'pr-get': return this.octokit!.rest.pulls.get({ owner, repo, pull_number, ...otherParams }); case 'pr-update': return this.octokit!.rest.pulls.update({ owner, repo, pull_number, ...otherParams }); case 'pr-merge': return this.octokit!.rest.pulls.merge({ owner, repo, pull_number, commit_title: params.commit_title, commit_message: params.commit_message, merge_method: params.merge_method || 'merge', ...otherParams }); case 'pr-close': return this.octokit!.rest.pulls.update({ owner, repo, pull_number, state: 'closed', ...otherParams }); case 'pr-review': return this.octokit!.rest.pulls.createReview({ owner, repo, pull_number, event: params.event, body: params.body, ...otherParams }); case 'pr-search': return this.octokit!.rest.search.issuesAndPullRequests({ 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.githubConfig.username, repo, ...otherParams } = params; switch (operation) { case 'branch-create': return this.octokit!.rest.git.createRef({ owner, repo, ref: `refs/heads/${params.branch}`, sha: params.sha, ...otherParams }); case 'branch-list': return this.octokit!.rest.repos.listBranches({ owner, repo, ...otherParams }); case 'branch-get': return this.octokit!.rest.repos.getBranch({ owner, repo, branch: params.branch, ...otherParams }); case 'branch-delete': return this.octokit!.rest.git.deleteRef({ owner, repo, ref: `heads/${params.branch}`, ...otherParams }); case 'branch-compare': return this.octokit!.rest.repos.compareCommits({ owner, repo, base: params.base, head: params.head, ...otherParams }); default: throw new Error(`Unsupported branch operation: ${operation}`); } } /** * Execute tag operations */ private async executeTagOperation(operation: string, params: any): Promise<any> { const { owner = this.githubConfig.username, repo, ...otherParams } = params; switch (operation) { case 'tag-create': return this.octokit!.rest.git.createTag({ owner, repo, tag: params.tag, message: params.message, object: params.object, type: params.type || 'commit', ...otherParams }); case 'tag-list': return this.octokit!.rest.repos.listTags({ owner, repo, ...otherParams }); case 'tag-get': return this.octokit!.rest.git.getTag({ owner, repo, tag_sha: params.tag_sha, ...otherParams }); case 'tag-delete': return this.octokit!.rest.git.deleteRef({ owner, repo, ref: `tags/${params.tag}`, ...otherParams }); case 'tag-search': return this.octokit!.rest.repos.listTags({ owner, repo, ...otherParams }); default: throw new Error(`Unsupported tag operation: ${operation}`); } } /** * Execute release operations */ private async executeReleaseOperation(operation: string, params: any): Promise<any> { const { owner = this.githubConfig.username, repo, release_id, tagName, ...otherParams } = params; switch (operation) { case 'release-create': return this.octokit!.rest.repos.createRelease({ owner, repo, tag_name: params.tag_name || tagName, name: params.name, body: params.body, draft: params.draft || false, prerelease: params.prerelease || false, ...otherParams }); case 'release-list': return this.octokit!.rest.repos.listReleases({ owner, repo, ...otherParams }); 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.octokit!.rest.repos.listReleases({ owner, repo }); const release = releases.data.find(r => r.tag_name === params.tagName); if (!release) { throw new Error(`Release with tag '${params.tagName}' not found`); } getReleaseId = release.id; } return this.octokit!.rest.repos.getRelease({ owner, repo, release_id: getReleaseId, ...otherParams }); 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.octokit!.rest.repos.listReleases({ owner, repo }); const release = releases.data.find(r => r.tag_name === params.tagName); if (!release) { throw new Error(`Release with tag '${params.tagName}' not found`); } updateReleaseId = release.id; } return this.octokit!.rest.repos.updateRelease({ owner, repo, release_id: 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.octokit!.rest.repos.listReleases({ owner, repo }); const release = releases.data.find(r => r.tag_name === params.tagName); if (!release) { throw new Error(`Release with tag '${params.tagName}' not found`); } deleteReleaseId = release.id; } return this.octokit!.rest.repos.deleteRelease({ owner, repo, release_id: deleteReleaseId, ...otherParams }); 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.octokit!.rest.repos.listReleases({ owner, repo }); const release = releases.data.find(r => r.tag_name === params.tagName); if (!release) { throw new Error(`Release with tag '${params.tagName}' not found`); } publishReleaseId = release.id; } return this.octokit!.rest.repos.updateRelease({ owner, repo, release_id: publishReleaseId, draft: false, ...otherParams }); case 'release-download': return this.octokit!.rest.repos.getReleaseAsset({ owner, repo, asset_id: params.asset_id, ...otherParams }); default: throw new Error(`Unsupported release operation: ${operation}`); } } /** * Execute file operations */ private async executeFileOperation(operation: string, params: any): Promise<any> { const { owner = this.githubConfig.username, repo, path, ...otherParams } = params; switch (operation) { case 'file-read': return this.octokit!.rest.repos.getContent({ owner, repo, path, ...otherParams }); // Compatibility alias: return directory listing or file content metadata case 'listFiles': { // If path is omitted, list repository root const listPath = path || ''; const response = await this.octokit!.rest.repos.getContent({ owner, repo, path: listPath, ...otherParams }); // If response is an array, it's a directory listing const data = Array.isArray(response.data) ? response.data.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.data.name, path: response.data.path, sha: response.data.sha, type: response.data.type, size: response.data.size, url: response.data.html_url || response.data.url }); return data; } // Compatibility alias: get raw file content and normalize return case 'getFile': { const res = await this.octokit!.rest.repos.getContent({ owner, repo, path, ...otherParams }); // GitHub returns base64 content for files if (!res || !res.data) { throw new Error('Empty response from GitHub getContent'); } // If this is a file object const fileData: any = Array.isArray(res.data) ? res.data[0] : res.data; const contentEncoded = fileData.content; let contentDecoded: string | null = null; if (contentEncoded) { contentDecoded = Buffer.from(contentEncoded, 'base64').toString('utf8'); } 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.octokit!.rest.search.code({ q: `${params.query} repo:${owner}/${repo}`, ...otherParams }); default: throw new Error(`Unsupported file operation: ${operation}`); } } /** * Execute package operations */ private async executePackageOperation(operation: string, params: any): Promise<any> { const { owner = this.githubConfig.username, ...otherParams } = params; switch (operation) { case 'package-list': return this.octokit!.rest.packages.listPackagesForUser({ username: owner, package_type: params.package_type || 'npm', ...otherParams }); case 'package-get': return this.octokit!.rest.packages.getPackageForUser({ username: owner, package_type: params.package_type || 'npm', package_name: params.package_name, ...otherParams }); case 'package-create': // Package creation is typically done through package managers, not GitHub API throw new Error('Package creation should be done through package managers (npm, etc.)'); case 'package-update': // Package updates are typically done through package managers throw new Error('Package updates should be done through package managers (npm, etc.)'); case 'package-delete': return this.octokit!.rest.packages.deletePackageForUser({ username: owner, package_type: params.package_type || 'npm', package_name: params.package_name, ...otherParams }); case 'package-publish': // Package publishing is typically done through package managers throw new Error('Package publishing should be done through package managers (npm, etc.)'); case 'package-download': return this.octokit!.rest.packages.getPackageVersionForUser({ username: owner, package_type: params.package_type || 'npm', package_name: params.package_name, package_version_id: params.package_version_id, ...otherParams }); default: throw new Error(`Unsupported package operation: ${operation}`); } } }

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