Skip to main content
Glama

github-manager MCP Server

github.ts9.87 kB
import { Octokit } from '@octokit/rest'; import fetch from 'node-fetch'; import { ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { GitHubCollaborator, GitHubError, GitHubOrg, GitHubRepo, GitHubRepoSettings, GitHubService, Logger } from '../types.js'; import { createValidationError } from './error/errorUtils.js'; export class OctokitGitHubService implements GitHubService { private octokit: Octokit; private logger: Logger; constructor(token: string, logger: Logger) { if (!token) { throw new Error('GitHub token is required'); } this.logger = logger; this.octokit = new Octokit({ auth: token, request: { fetch: fetch as any } }); this.logger.debug('GitHub service initialized'); } async verifyAuth(): Promise<{ username: string; scopes: string[] }> { try { const { data: user, headers } = await this.octokit.users.getAuthenticated(); const scopes = headers['x-oauth-scopes'] || ''; const scopesList = scopes.split(',').map(s => s.trim()); this.logger.info('Successfully authenticated as GitHub user', { username: user.login, scopes: scopesList }); return { username: user.login, scopes: scopesList }; } catch (error: any) { const gitHubError: GitHubError = new Error('Unable to authenticate') as GitHubError; gitHubError.code = ErrorCode.InternalError; gitHubError.details = { action: 'verify_auth', originalError: { status: error.status, message: error.message, response: error.response }, status: error.status, requestId: error.response?.headers?.['x-github-request-id'], documentation: error.response?.data?.documentation_url }; this.logger.error('Authentication failed', { status: error.status, message: error.message, requestId: gitHubError.details.requestId }); throw gitHubError; } } async listOrgs(): Promise<GitHubOrg[]> { try { // Get user info and scopes const userResponse = await this.octokit.users.getAuthenticated(); const scopes = userResponse.headers['x-oauth-scopes'] || ''; const scopesList = scopes.split(',').map(s => s.trim()); // Check for required scope if (!scopesList.includes('read:org')) { const error: GitHubError = new Error( 'Insufficient permissions to list organization memberships' ) as GitHubError; error.code = ErrorCode.InvalidParams; error.details = { action: 'list_orgs', originalError: { status: 403, message: 'Insufficient permissions', response: { data: { message: 'Insufficient permissions', documentation_url: 'https://docs.github.com/rest/orgs/orgs#list-organizations-for-the-authenticated-user' }, headers: {} } }, required_scopes: ['read:org'], current_scopes: scopesList, documentation: 'https://docs.github.com/rest/orgs/orgs#list-organizations-for-the-authenticated-user' }; this.logger.error('Missing required scope', { required: 'read:org', current: scopesList }); throw error; } // Get organizations using both methods const [memberOrgsResponse, visibleOrgsResponse] = await Promise.all([ this.octokit.orgs.listForAuthenticatedUser({ per_page: 100 }), this.octokit.orgs.list({ per_page: 100 }) ]); // Combine and deduplicate organizations const allOrgs = new Map<string, GitHubOrg>(); [...memberOrgsResponse.data, ...visibleOrgsResponse.data].forEach(org => { if (!allOrgs.has(org.login)) { allOrgs.set(org.login, { name: org.login, display_name: org.login, description: org.description || undefined, url: org.url, membership: { is_member: memberOrgsResponse.data.some(memberOrg => memberOrg.login === org.login), is_visible: visibleOrgsResponse.data.some(visibleOrg => visibleOrg.login === org.login) } }); } }); return Array.from(allOrgs.values()); } catch (error: any) { // Re-throw if it's already a GitHubError if (error instanceof Error && 'code' in error) { throw error; } throw createValidationError('Unable to list organizations. Please check your permissions.', { action: 'list_orgs', status: error.status, message: error.message, requestId: error.response?.headers?.['x-github-request-id'], documentation: error.response?.data?.documentation_url }); } } async listRepos(org: string): Promise<GitHubRepo[]> { try { const { data: repos } = await this.octokit.repos.listForOrg({ org, sort: 'updated', direction: 'desc' }); return repos.map(repo => { if (!repo.clone_url) { throw createValidationError('Failed to get repository clone URL', { action: 'list_repos', organization: org, repository: repo.name }); } return { name: repo.name, description: repo.description || undefined, private: repo.private, url: repo.html_url, clone_url: repo.clone_url }; }); } catch (error: any) { const isRateLimit = error.status === 403 && error.response?.data?.message?.includes('rate limit'); const resetTime = error.response?.headers?.['x-ratelimit-reset']; const resetDate = resetTime ? new Date(parseInt(resetTime) * 1000).toLocaleString() : 'unknown time'; const errorMessage = isRateLimit ? `GitHub API rate limit exceeded. You can try again after ${resetDate}` : 'Unable to list repositories. Please check your permissions.'; throw createValidationError(errorMessage, { action: 'list_repos', organization: org, status: error.status, message: error.message, requestId: error.response?.headers?.['x-github-request-id'], documentation: error.response?.data?.documentation_url, rate_limit: isRateLimit ? { remaining: error.response?.headers?.['x-ratelimit-remaining'], reset: error.response?.headers?.['x-ratelimit-reset'] } : undefined }); } } async createRepo(org: string, name: string, description?: string, isPrivate = false): Promise<GitHubRepo> { try { const { data: repo } = await this.octokit.repos.createInOrg({ org, name, description, private: isPrivate }); if (!repo.clone_url) { throw createValidationError('Repository created but missing clone URL', { action: 'create_repo', repository: { org, name } }); } return { name: repo.name, description: repo.description || undefined, private: repo.private, url: repo.html_url, clone_url: repo.clone_url }; } catch (error: any) { throw createValidationError( `Unable to create repository '${name}' in organization '${org}'. ${error.response?.data?.message || ''}`, { action: 'create_repo', status: error.status, message: error.message, requestId: error.response?.headers?.['x-github-request-id'], documentation: error.response?.data?.documentation_url, repository: { org, name, description, isPrivate } } ); } } async addCollaborator(org: string, repo: string, username: string, permission: 'pull' | 'push' | 'admin'): Promise<GitHubCollaborator> { try { const { data } = await this.octokit.repos.addCollaborator({ owner: org, repo, username, permission }); return { status: 'success', invitation_url: data.html_url }; } catch (error: any) { throw createValidationError( `Unable to add collaborator '${username}' to repository '${org}/${repo}'. ${error.response?.data?.message || ''}`, { action: 'add_collaborator', status: error.status, message: error.message, requestId: error.response?.headers?.['x-github-request-id'], documentation: error.response?.data?.documentation_url, collaborator: { org, repo, username, permission } } ); } } async updateRepoSettings(org: string, repo: string, settings: GitHubRepoSettings['settings']): Promise<GitHubRepoSettings> { try { const { data } = await this.octokit.repos.update({ owner: org, repo, ...settings }); return { name: data.name, settings: { has_issues: data.has_issues, has_projects: data.has_projects, has_wiki: data.has_wiki, allow_squash_merge: data.allow_squash_merge, allow_merge_commit: data.allow_merge_commit, allow_rebase_merge: data.allow_rebase_merge } }; } catch (error: any) { throw createValidationError( `Unable to update settings for repository '${org}/${repo}'. ${error.response?.data?.message || ''}`, { action: 'update_repo_settings', status: error.status, message: error.message, requestId: error.response?.headers?.['x-github-request-id'], documentation: error.response?.data?.documentation_url, repository: { org, repo }, settings: settings } ); } } }

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/wheelhousedev/github-mcp'

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