Skip to main content
Glama
git-remote.ts23.5 kB
/** * Git Remote Tool * * Comprehensive Git remote management tool providing remote repository operations. * Supports add, remove, rename, show, set-url, and prune operations. * * Operations: add, remove, rename, show, set-url, prune */ import { GitCommandExecutor, GitCommandResult } from '../utils/git-command-executor.js'; import { ParameterValidator, ToolParams } from '../utils/parameter-validator.js'; import { OperationErrorHandler, ToolResult } from '../utils/operation-error-handler.js'; import { configManager } from '../config.js'; export interface GitRemoteParams extends ToolParams { action: 'add' | 'remove' | 'rename' | 'show' | 'set-url' | 'prune' | 'list'; // Remote parameters name?: string; // Remote name (required for most operations) url?: string; // Remote URL (required for add, set-url) newName?: string; // New remote name (required for rename) // Options fetch?: boolean; // Fetch after add (for add action) push?: boolean; // Set push URL (for set-url action) all?: boolean; // Show all remotes (for show action) verbose?: boolean; // Verbose output (for show action) dryRun?: boolean; // Dry run (for prune action) } export class GitRemoteTool { private gitExecutor: GitCommandExecutor; constructor() { this.gitExecutor = new GitCommandExecutor(); } /** * Execute git-remote operation */ async execute(params: GitRemoteParams): Promise<ToolResult> { const startTime = Date.now(); try { // Validate basic parameters const validation = ParameterValidator.validateToolParams('git-remote', params); if (!validation.isValid) { return OperationErrorHandler.createToolError( 'VALIDATION_ERROR', `Parameter validation failed: ${validation.errors.join(', ')}`, params.action, { validationErrors: validation.errors }, validation.suggestions ); } // Validate operation-specific parameters const operationValidation = this.validateOperationParams(params); if (!operationValidation.isValid) { return OperationErrorHandler.createToolError( 'VALIDATION_ERROR', `Operation validation failed: ${operationValidation.errors.join(', ')}`, params.action, { validationErrors: operationValidation.errors }, operationValidation.suggestions ); } // Check if it's a Git repository const isRepo = await this.gitExecutor.isGitRepository(params.projectPath); if (!isRepo) { return OperationErrorHandler.createToolError( 'NOT_A_GIT_REPOSITORY', 'The specified path is not a Git repository', params.action, { projectPath: params.projectPath }, ['Initialize a Git repository first with: git init'] ); } // Route to appropriate handler switch (params.action) { case 'add': return await this.handleAdd(params, startTime); case 'remove': return await this.handleRemove(params, startTime); case 'rename': return await this.handleRename(params, startTime); case 'show': return await this.handleShow(params, startTime); case 'set-url': return await this.handleSetUrl(params, startTime); case 'prune': return await this.handlePrune(params, startTime); case 'list': return await this.handleList(params, startTime); default: return OperationErrorHandler.createToolError( 'UNSUPPORTED_OPERATION', `Operation '${params.action}' is not supported`, params.action, {}, ['Use one of: add, remove, rename, show, set-url, prune, list'] ); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'EXECUTION_ERROR', `Failed to execute ${params.action}: ${errorMessage}`, params.action, { error: errorMessage }, ['Check the error details and try again'] ); } } /** * Validate operation-specific parameters */ private validateOperationParams(params: GitRemoteParams): { isValid: boolean; errors: string[]; suggestions: string[] } { const errors: string[] = []; const suggestions: string[] = []; switch (params.action) { case 'add': if (!params.name) { errors.push('Remote name is required for add operation'); suggestions.push('Provide a remote name (e.g., "origin", "upstream")'); } if (!params.url) { errors.push('Remote URL is required for add operation'); suggestions.push('Provide a remote URL (e.g., "https://github.com/user/repo.git")'); } break; case 'remove': if (!params.name) { errors.push('Remote name is required for remove operation'); suggestions.push('Provide the name of the remote to remove'); } break; case 'rename': if (!params.name) { errors.push('Current remote name is required for rename operation'); suggestions.push('Provide the current remote name'); } if (!params.newName) { errors.push('New remote name is required for rename operation'); suggestions.push('Provide the new remote name'); } break; case 'set-url': if (!params.name) { errors.push('Remote name is required for set-url operation'); suggestions.push('Provide the remote name to update'); } if (!params.url) { errors.push('Remote URL is required for set-url operation'); suggestions.push('Provide the new remote URL'); } break; case 'prune': if (!params.name) { errors.push('Remote name is required for prune operation'); suggestions.push('Provide the remote name to prune (e.g., "origin")'); } break; case 'show': // Show operation can work without parameters (shows all remotes) break; } return { isValid: errors.length === 0, errors, suggestions }; } /** * Handle git remote add operation */ private async handleAdd(params: GitRemoteParams, startTime: number): Promise<ToolResult> { try { const args = [params.name!, params.url!]; // Add fetch option if specified if (params.fetch) { args.unshift('-f'); } const result = await this.gitExecutor.executeGitCommand('remote', ['add', ...args], params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'remote add', params.projectPath); } // If fetch was requested, the output will include fetch information const message = params.fetch ? `Remote '${params.name}' added and fetched successfully` : `Remote '${params.name}' added successfully`; return { success: true, data: { message, remoteName: params.name, remoteUrl: params.url, fetched: params.fetch || false, output: result.stdout }, metadata: { operation: 'remote add', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'REMOTE_ADD_ERROR', `Failed to add remote: ${errorMessage}`, 'add', { error: errorMessage, remoteName: params.name, remoteUrl: params.url } ); } } /** * Handle git remote remove operation */ private async handleRemove(params: GitRemoteParams, startTime: number): Promise<ToolResult> { try { // Check if remote exists first const showResult = await this.gitExecutor.executeGitCommand('remote', ['show'], params.projectPath); if (showResult.success) { const remotes = showResult.stdout.split('\n').filter(line => line.trim()); if (!remotes.includes(params.name!)) { return OperationErrorHandler.createToolError( 'REMOTE_NOT_FOUND', `Remote '${params.name}' does not exist`, 'remove', { remoteName: params.name, availableRemotes: remotes }, [`Available remotes: ${remotes.join(', ')}`] ); } } const result = await this.gitExecutor.executeGitCommand('remote', ['remove', params.name!], params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'remote remove', params.projectPath); } return { success: true, data: { message: `Remote '${params.name}' removed successfully`, remoteName: params.name, output: result.stdout }, metadata: { operation: 'remote remove', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'REMOTE_REMOVE_ERROR', `Failed to remove remote: ${errorMessage}`, 'remove', { error: errorMessage, remoteName: params.name } ); } } /** * Handle git remote rename operation */ private async handleRename(params: GitRemoteParams, startTime: number): Promise<ToolResult> { try { // Check if current remote exists const showResult = await this.gitExecutor.executeGitCommand('remote', ['show'], params.projectPath); if (showResult.success) { const remotes = showResult.stdout.split('\n').filter(line => line.trim()); if (!remotes.includes(params.name!)) { return OperationErrorHandler.createToolError( 'REMOTE_NOT_FOUND', `Remote '${params.name}' does not exist`, 'rename', { remoteName: params.name, availableRemotes: remotes }, [`Available remotes: ${remotes.join(', ')}`] ); } if (remotes.includes(params.newName!)) { return OperationErrorHandler.createToolError( 'REMOTE_ALREADY_EXISTS', `Remote '${params.newName}' already exists`, 'rename', { newRemoteName: params.newName, availableRemotes: remotes }, ['Choose a different name for the remote'] ); } } const result = await this.gitExecutor.executeGitCommand('remote', ['rename', params.name!, params.newName!], params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'remote rename', params.projectPath); } return { success: true, data: { message: `Remote '${params.name}' renamed to '${params.newName}' successfully`, oldName: params.name, newName: params.newName, output: result.stdout }, metadata: { operation: 'remote rename', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'REMOTE_RENAME_ERROR', `Failed to rename remote: ${errorMessage}`, 'rename', { error: errorMessage, oldName: params.name, newName: params.newName } ); } } /** * Handle git remote show operation */ private async handleShow(params: GitRemoteParams, startTime: number): Promise<ToolResult> { try { const args: string[] = []; if (params.verbose) { args.push('-v'); } if (params.name && !params.all) { // Show specific remote details args.push(params.name); } const result = await this.gitExecutor.executeGitCommand('remote', ['show', ...args], params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'remote show', params.projectPath); } // Parse the output const output = result.stdout.trim(); let parsedData: any = { raw: output }; if (params.name && !params.all) { // Detailed remote information parsedData.remoteName = params.name; parsedData.details = this.parseRemoteDetails(output); } else { // List of remotes const lines = output.split('\n').filter(line => line.trim()); if (params.verbose) { parsedData.remotes = this.parseVerboseRemotes(lines); } else { parsedData.remotes = lines; } } return { success: true, data: { message: params.name ? `Remote '${params.name}' details` : 'Remote repositories', ...parsedData }, metadata: { operation: 'remote show', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'REMOTE_SHOW_ERROR', `Failed to show remote information: ${errorMessage}`, 'show', { error: errorMessage, remoteName: params.name } ); } } /** * Handle git remote set-url operation */ private async handleSetUrl(params: GitRemoteParams, startTime: number): Promise<ToolResult> { try { // Check if remote exists const showResult = await this.gitExecutor.executeGitCommand('remote', ['show'], params.projectPath); if (showResult.success) { const remotes = showResult.stdout.split('\n').filter(line => line.trim()); if (!remotes.includes(params.name!)) { return OperationErrorHandler.createToolError( 'REMOTE_NOT_FOUND', `Remote '${params.name}' does not exist`, 'set-url', { remoteName: params.name, availableRemotes: remotes }, [`Available remotes: ${remotes.join(', ')}`] ); } } const args = [params.name!, params.url!]; // Add push option if specified if (params.push) { args.unshift('--push'); } const result = await this.gitExecutor.executeGitCommand('remote', ['set-url', ...args], params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'remote set-url', params.projectPath); } const urlType = params.push ? 'push URL' : 'URL'; return { success: true, data: { message: `Remote '${params.name}' ${urlType} updated successfully`, remoteName: params.name, newUrl: params.url, urlType: params.push ? 'push' : 'fetch', output: result.stdout }, metadata: { operation: 'remote set-url', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'REMOTE_SET_URL_ERROR', `Failed to set remote URL: ${errorMessage}`, 'set-url', { error: errorMessage, remoteName: params.name, url: params.url } ); } } /** * Handle git remote prune operation */ private async handlePrune(params: GitRemoteParams, startTime: number): Promise<ToolResult> { try { // Check if remote exists const showResult = await this.gitExecutor.executeGitCommand('remote', ['show'], params.projectPath); if (showResult.success) { const remotes = showResult.stdout.split('\n').filter(line => line.trim()); if (!remotes.includes(params.name!)) { return OperationErrorHandler.createToolError( 'REMOTE_NOT_FOUND', `Remote '${params.name}' does not exist`, 'prune', { remoteName: params.name, availableRemotes: remotes }, [`Available remotes: ${remotes.join(', ')}`] ); } } const args = [params.name!]; // Add dry-run option if specified if (params.dryRun) { args.unshift('--dry-run'); } const result = await this.gitExecutor.executeGitCommand('remote', ['prune', ...args], params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'remote prune', params.projectPath); } // Parse pruned branches from output const prunedBranches = this.parsePrunedBranches(result.stdout); const message = params.dryRun ? `Dry run: Would prune ${prunedBranches.length} stale branches from '${params.name}'` : `Pruned ${prunedBranches.length} stale branches from '${params.name}'`; return { success: true, data: { message, remoteName: params.name, dryRun: params.dryRun || false, prunedBranches, output: result.stdout }, metadata: { operation: 'remote prune', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'REMOTE_PRUNE_ERROR', `Failed to prune remote: ${errorMessage}`, 'prune', { error: errorMessage, remoteName: params.name } ); } } /** * Handle list remotes */ private async handleList(params: GitRemoteParams, startTime: number): Promise<ToolResult> { try { const result = await this.gitExecutor.executeGitCommand('remote', [], params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'remote list', params.projectPath); } // Parse remotes from output const remotes = result.stdout .split('\n') .map(line => line.trim()) .filter(line => line.length > 0); return { success: true, data: { operation: 'list', message: `Found ${remotes.length} remote(s)`, details: { remotes, count: remotes.length }, duration: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'REMOTE_LIST_ERROR', `Failed to list remotes: ${errorMessage}`, 'list', { error: errorMessage } ); } } /** * Parse remote details from git remote show output */ private parseRemoteDetails(output: string): any { const lines = output.split('\n'); const details: any = {}; for (const line of lines) { const trimmed = line.trim(); if (trimmed.includes('Fetch URL:')) { details.fetchUrl = trimmed.split('Fetch URL:')[1]?.trim(); } else if (trimmed.includes('Push URL:')) { details.pushUrl = trimmed.split('Push URL:')[1]?.trim(); } else if (trimmed.includes('HEAD branch:')) { details.headBranch = trimmed.split('HEAD branch:')[1]?.trim(); } } return details; } /** * Parse verbose remotes output */ private parseVerboseRemotes(lines: string[]): Array<{ name: string; fetchUrl?: string; pushUrl?: string }> { const remotes: Array<{ name: string; fetchUrl?: string; pushUrl?: string }> = []; const remoteMap = new Map<string, { name: string; fetchUrl?: string; pushUrl?: string }>(); for (const line of lines) { const parts = line.split('\t'); if (parts.length >= 2) { const name = parts[0].trim(); const urlAndType = parts[1].trim(); const urlMatch = urlAndType.match(/^(.+?)\s+\((.+)\)$/); if (urlMatch) { const url = urlMatch[1]; const type = urlMatch[2]; if (!remoteMap.has(name)) { remoteMap.set(name, { name }); } const remote = remoteMap.get(name)!; if (type === 'fetch') { remote.fetchUrl = url; } else if (type === 'push') { remote.pushUrl = url; } } } } return Array.from(remoteMap.values()); } /** * Parse pruned branches from git remote prune output */ private parsePrunedBranches(output: string): string[] { const lines = output.split('\n'); const prunedBranches: string[] = []; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('* [would prune]') || trimmed.startsWith('* [pruned]')) { const branchMatch = trimmed.match(/\* \[(would prune|pruned)\] (.+)/); if (branchMatch) { prunedBranches.push(branchMatch[2]); } } } return prunedBranches; } /** * Get tool schema for MCP registration */ static getToolSchema() { return { name: 'git-remote', description: 'Git remote management tool for managing remote repositories. Supports add, remove, rename, show, set-url, and prune operations.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['add', 'remove', 'rename', 'show', 'set-url', 'prune', 'list'], description: 'The remote operation to perform' }, projectPath: { type: 'string', description: 'Absolute path to the project directory' }, name: { type: 'string', description: 'Remote name (required for most operations)' }, url: { type: 'string', description: 'Remote URL (required for add and set-url operations)' }, newName: { type: 'string', description: 'New remote name (required for rename operation)' }, fetch: { type: 'boolean', description: 'Fetch after adding remote (for add action)' }, push: { type: 'boolean', description: 'Set push URL instead of fetch URL (for set-url action)' }, all: { type: 'boolean', description: 'Show all remotes (for show action)' }, verbose: { type: 'boolean', description: 'Show verbose output with URLs (for show action)' }, dryRun: { type: 'boolean', description: 'Show what would be pruned without actually pruning (for prune action)' } }, required: ['action', 'projectPath'] } }; } }

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