/**
* 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}`);
}
}
}