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