Skip to main content
Glama
config.ts16.1 kB
/** * Configuration Management * * Handles environment variable configuration for GitHub and Gitea providers. * Provides validation and auto-detection utilities. */ import * as path from 'path'; import * as fs from 'fs'; import { Octokit } from '@octokit/rest'; import axios from 'axios'; export type GitMCPMode = 'normal' | 'universal'; export interface GitMCPConfig { mode: GitMCPMode; github?: { token: string; username: string; }; gitea?: { url: string; token: string; username: string; }; } export interface ProjectContext { projectPath: string; repositoryName: string; isGitRepository: boolean; remoteOrigin?: string; currentBranch?: string; detectedProvider?: string; } export interface ConfigValidationResult { isValid: boolean; errors: string[]; warnings: string[]; missingEnvVars: string[]; configurationGuide?: { requiredEnvVars: string[]; exampleConfig: Record<string, string>; }; } export interface DetailedValidationResult { isValid: boolean; providerResults: { github?: ProviderValidationResult; gitea?: ProviderValidationResult; }; summary: { totalProviders: number; validProviders: number; errors: string[]; warnings: string[]; }; } export interface ProviderValidationResult { provider: 'github' | 'gitea'; isValid: boolean; isConfigured: boolean; credentialStatus: 'valid' | 'invalid' | 'missing' | 'network_error'; errorDetails?: string; userInfo?: { username: string; displayName?: string; email?: string; }; permissions?: string[]; rateLimit?: { limit: number; remaining: number; reset: Date; }; } export class ConfigManager { private config: GitMCPConfig; constructor() { this.config = this.loadConfiguration(); } private loadConfiguration(): GitMCPConfig { const config: GitMCPConfig = { mode: (process.env.GIT_MCP_MODE as GitMCPMode) || 'normal' }; // GitHub configuration if (process.env.GITHUB_TOKEN && process.env.GITHUB_USERNAME) { config.github = { token: process.env.GITHUB_TOKEN, username: process.env.GITHUB_USERNAME, }; } // Gitea configuration if (process.env.GITEA_URL && process.env.GITEA_TOKEN && process.env.GITEA_USERNAME) { config.gitea = { url: process.env.GITEA_URL, token: process.env.GITEA_TOKEN, username: process.env.GITEA_USERNAME, }; } return config; } public getConfig(): GitMCPConfig { return this.config; } public isGitHubConfigured(): boolean { return !!this.config.github; } public isGiteaConfigured(): boolean { return !!this.config.gitea; } public getConfigurationStatus(): Record<string, string> { return { github: this.isGitHubConfigured() ? 'configured' : 'not configured', gitea: this.isGiteaConfigured() ? 'configured' : 'not configured', }; } /** * Gets the current operation mode */ public getMode(): GitMCPMode { return this.config.mode; } /** * Checks if universal mode is enabled */ public isUniversalMode(): boolean { return this.config.mode === 'universal'; } /** * Validates GitHub and Gitea credentials */ public validateCredentials(): ConfigValidationResult { const result: ConfigValidationResult = { isValid: true, errors: [], warnings: [], missingEnvVars: [], }; // Check GitHub configuration if (process.env.GITHUB_TOKEN && !process.env.GITHUB_USERNAME) { result.errors.push('GITHUB_TOKEN is set but GITHUB_USERNAME is missing'); result.missingEnvVars.push('GITHUB_USERNAME'); result.isValid = false; } if (process.env.GITHUB_USERNAME && !process.env.GITHUB_TOKEN) { result.errors.push('GITHUB_USERNAME is set but GITHUB_TOKEN is missing'); result.missingEnvVars.push('GITHUB_TOKEN'); result.isValid = false; } // Check Gitea configuration const giteaVars = ['GITEA_URL', 'GITEA_TOKEN', 'GITEA_USERNAME']; const setGiteaVars = giteaVars.filter(varName => !!process.env[varName]); if (setGiteaVars.length > 0 && setGiteaVars.length < 3) { const missingGiteaVars = giteaVars.filter(varName => !process.env[varName]); result.errors.push(`Partial Gitea configuration detected. Missing: ${missingGiteaVars.join(', ')}`); result.missingEnvVars.push(...missingGiteaVars); result.isValid = false; } // Validate Gitea URL format if (process.env.GITEA_URL) { try { new URL(process.env.GITEA_URL); } catch (error) { result.errors.push('GITEA_URL is not a valid URL format'); result.isValid = false; } } // Check if at least one provider is configured if (!this.isGitHubConfigured() && !this.isGiteaConfigured()) { result.warnings.push('No providers are configured. At least one provider (GitHub or Gitea) should be configured for remote operations.'); result.configurationGuide = { requiredEnvVars: ['GITHUB_TOKEN', 'GITHUB_USERNAME', 'GITEA_URL', 'GITEA_TOKEN', 'GITEA_USERNAME'], exampleConfig: { 'GITHUB_TOKEN': 'ghp_xxxxxxxxxxxxxxxxxxxx', 'GITHUB_USERNAME': 'your-github-username', 'GITEA_URL': 'https://gitea.example.com', 'GITEA_TOKEN': 'your-gitea-token', 'GITEA_USERNAME': 'your-gitea-username' } }; } // Validate universal mode requirements if (this.config.mode === 'universal') { if (!this.isGitHubConfigured() || !this.isGiteaConfigured()) { result.errors.push('Universal mode requires both GitHub and Gitea to be configured'); result.isValid = false; result.missingEnvVars.push(...(!this.isGitHubConfigured() ? ['GITHUB_TOKEN', 'GITHUB_USERNAME'] : [])); result.missingEnvVars.push(...(!this.isGiteaConfigured() ? ['GITEA_URL', 'GITEA_TOKEN', 'GITEA_USERNAME'] : [])); } } return result; } /** * Auto-detects project context from projectPath */ public detectProjectContext(projectPath: string): ProjectContext { const context: ProjectContext = { projectPath, repositoryName: path.basename(projectPath), isGitRepository: false, }; try { // Check if it's a Git repository const gitDir = path.join(projectPath, '.git'); context.isGitRepository = fs.existsSync(gitDir); if (context.isGitRepository) { // Try to detect remote origin const gitConfigPath = path.join(gitDir, 'config'); if (fs.existsSync(gitConfigPath)) { const gitConfig = fs.readFileSync(gitConfigPath, 'utf8'); const remoteMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url = (.+)/); if (remoteMatch) { context.remoteOrigin = remoteMatch[1].trim(); // Detect provider from remote URL if (context.remoteOrigin.includes('github.com')) { context.detectedProvider = 'github'; } else if (context.remoteOrigin.includes('gitea') || (this.config.gitea && context.remoteOrigin.includes(new URL(this.config.gitea.url).hostname))) { context.detectedProvider = 'gitea'; } } } // Try to detect current branch const headPath = path.join(gitDir, 'HEAD'); if (fs.existsSync(headPath)) { const headContent = fs.readFileSync(headPath, 'utf8').trim(); const branchMatch = headContent.match(/ref: refs\/heads\/(.+)/); if (branchMatch) { context.currentBranch = branchMatch[1]; } } } } catch (error) { // Silently handle errors in detection } return context; } /** * Auto-detects username/owner from environment variables */ public autoDetectUsername(provider: 'github' | 'gitea'): string | undefined { if (provider === 'github' && this.config.github) { return this.config.github.username; } if (provider === 'gitea' && this.config.gitea) { return this.config.gitea.username; } return undefined; } /** * Gets provider configuration for a specific provider */ public getProviderConfig(provider: 'github' | 'gitea') { return provider === 'github' ? this.config.github : this.config.gitea; } /** * Checks if a provider is supported for operations */ public isProviderSupported(provider: 'github' | 'gitea' | 'both'): boolean { if (provider === 'both') { return this.isGitHubConfigured() && this.isGiteaConfigured(); } return provider === 'github' ? this.isGitHubConfigured() : this.isGiteaConfigured(); } /** * Validates credentials with detailed testing of API connectivity */ public async validateCredentialsDetailed(): Promise<DetailedValidationResult> { const result: DetailedValidationResult = { isValid: false, providerResults: {}, summary: { totalProviders: 0, validProviders: 0, errors: [], warnings: [] } }; // Validate GitHub credentials if (this.isGitHubConfigured()) { result.summary.totalProviders++; result.providerResults.github = await this.validateGitHubCredentials(); if (result.providerResults.github.isValid) { result.summary.validProviders++; } else { result.summary.errors.push(`GitHub: ${result.providerResults.github.errorDetails || 'Validation failed'}`); } } // Validate Gitea credentials if (this.isGiteaConfigured()) { result.summary.totalProviders++; result.providerResults.gitea = await this.validateGiteaCredentials(); if (result.providerResults.gitea.isValid) { result.summary.validProviders++; } else { result.summary.errors.push(`Gitea: ${result.providerResults.gitea.errorDetails || 'Validation failed'}`); } } result.isValid = result.summary.validProviders > 0; // Add warnings for partial configuration if (result.summary.totalProviders > 0 && result.summary.validProviders < result.summary.totalProviders) { result.summary.warnings.push('Some providers failed validation. Check credentials and network connectivity.'); } if (result.summary.totalProviders === 0) { result.summary.errors.push('No providers are configured. Set up GitHub or Gitea credentials.'); } // Add warning for universal mode with partial configuration if (this.config.mode === 'universal' && result.summary.totalProviders === 1) { result.summary.warnings.push('Universal mode is enabled but only one provider is configured. Both GitHub and Gitea are required for universal mode.'); } return result; } /** * Validates GitHub credentials with API test */ private async validateGitHubCredentials(): Promise<ProviderValidationResult> { const result: ProviderValidationResult = { provider: 'github', isValid: false, isConfigured: true, credentialStatus: 'invalid' }; if (!this.config.github?.token) { result.credentialStatus = 'missing'; result.errorDetails = 'GitHub token is not configured'; return result; } try { const octokit = new Octokit({ auth: this.config.github.token }); // Test authentication by getting user info const userResponse = await octokit.rest.users.getAuthenticated(); result.isValid = true; result.credentialStatus = 'valid'; result.userInfo = { username: userResponse.data.login, displayName: userResponse.data.name || undefined, email: userResponse.data.email || undefined }; // Get rate limit info try { const rateLimitResponse = await octokit.rest.rateLimit.get(); result.rateLimit = { limit: rateLimitResponse.data.rate.limit, remaining: rateLimitResponse.data.rate.remaining, reset: new Date(rateLimitResponse.data.rate.reset * 1000) }; } catch (rateLimitError) { // Rate limit check failed, but auth is still valid } } catch (error: any) { if (error.status === 401) { result.credentialStatus = 'invalid'; result.errorDetails = 'Invalid GitHub token or insufficient permissions'; } else if (error.status === 403) { result.credentialStatus = 'invalid'; result.errorDetails = 'GitHub token is valid but lacks required permissions'; } else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') { result.credentialStatus = 'network_error'; result.errorDetails = 'Network error: Cannot reach GitHub API'; } else { result.credentialStatus = 'invalid'; result.errorDetails = `GitHub API error: ${error.message}`; } } return result; } /** * Validates Gitea credentials with API test */ private async validateGiteaCredentials(): Promise<ProviderValidationResult> { const result: ProviderValidationResult = { provider: 'gitea', isValid: false, isConfigured: true, credentialStatus: 'invalid' }; if (!this.config.gitea?.url || !this.config.gitea?.token) { result.credentialStatus = 'missing'; result.errorDetails = 'Gitea URL or token is not configured'; return result; } try { // Test authentication by getting user info const response = await axios.get(`${this.config.gitea.url}/api/v1/user`, { headers: { 'Authorization': `token ${this.config.gitea.token}`, 'Accept': 'application/json' }, timeout: 10000 }); result.isValid = true; result.credentialStatus = 'valid'; result.userInfo = { username: response.data.login, displayName: response.data.full_name || undefined, email: response.data.email || undefined }; } catch (error: any) { if (error.response?.status === 401) { result.credentialStatus = 'invalid'; result.errorDetails = 'Invalid Gitea token'; } else if (error.response?.status === 403) { result.credentialStatus = 'invalid'; result.errorDetails = 'Gitea token is valid but lacks required permissions'; } else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') { result.credentialStatus = 'network_error'; result.errorDetails = `Network error: Cannot reach Gitea at ${this.config.gitea.url}`; } else if (error.response?.status >= 500) { result.credentialStatus = 'network_error'; result.errorDetails = `Gitea server error: ${error.response.status}`; } else { result.credentialStatus = 'invalid'; result.errorDetails = `Gitea API error: ${error.message}`; } } return result; } /** * Gets configuration guidance for missing setup */ public getConfigurationGuidance(): { requiredEnvVars: string[]; exampleConfig: Record<string, string>; setupInstructions: string[]; } { return { requiredEnvVars: [ 'GITHUB_TOKEN', 'GITHUB_USERNAME', 'GITEA_URL', 'GITEA_TOKEN', 'GITEA_USERNAME' ], exampleConfig: { 'GITHUB_TOKEN': 'ghp_xxxxxxxxxxxxxxxxxxxx', 'GITHUB_USERNAME': 'your-github-username', 'GITEA_URL': 'https://gitea.example.com', 'GITEA_TOKEN': 'your-gitea-token', 'GITEA_USERNAME': 'your-gitea-username' }, setupInstructions: [ '1. For GitHub: Create a personal access token at https://github.com/settings/tokens', '2. For Gitea: Create an access token in your Gitea instance settings', '3. Set the environment variables before running the MCP server', '4. At least one provider (GitHub or Gitea) must be configured for remote operations' ] }; } } // Export singleton instance and convenience functions export const configManager = new ConfigManager(); export function getProviderConfig(): GitMCPConfig { return configManager.getConfig(); }

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