/**
* Git Pull Requests Tool
*
* Comprehensive pull request management tool for Git repositories.
* Supports complete PR lifecycle: create, list, get, update, merge, close, review, search.
* Integrates with GitHub and Gitea providers for remote PR operations.
*/
import { ParameterValidator, ToolParams } from '../utils/parameter-validator.js';
import { OperationErrorHandler, ToolResult } from '../utils/operation-error-handler.js';
import { ProviderOperationHandler, OperationResult } from '../providers/provider-operation-handler.js';
import { ProviderConfig, ProviderOperation } from '../providers/types.js';
import { configManager } from '../config.js';
export interface GitPullsParams extends ToolParams {
action: 'create' | 'list' | 'get' | 'update' | 'merge' | 'close' | 'review' | 'search';
// Repository identification
repo?: string; // Repository name (auto-detected if not provided)
// Pull request identification
pull_number?: number; // Pull request number (for get/update/merge/close/review)
// Pull request creation/update parameters
title?: string; // PR title (required for create)
body?: string; // PR body/description
head?: string; // Source branch (required for create)
base?: string; // Target branch (required for create)
state?: 'open' | 'closed'; // PR state (for update)
// Listing parameters
state_filter?: 'open' | 'closed' | 'all'; // Filter PRs by state (for list)
sort?: 'created' | 'updated' | 'popularity' | 'long-running'; // Sort criteria (for list)
direction?: 'asc' | 'desc'; // Sort direction (for list)
head_filter?: string; // Filter by head branch (for list)
base_filter?: string; // Filter by base branch (for list)
// Merge parameters
commit_title?: string; // Merge commit title (for merge)
commit_message?: string; // Merge commit message (for merge)
merge_method?: 'merge' | 'squash' | 'rebase'; // Merge method (for merge)
// Review parameters
event?: 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT'; // Review event (for review)
review_body?: string; // Review comment body (for review)
// Search parameters
query?: string; // Search query (for search)
search_sort?: 'created' | 'updated' | 'popularity'; // Sort for search results
search_order?: 'asc' | 'desc'; // Order for search results
}
export class GitPullsTool {
private providerHandler?: ProviderOperationHandler;
constructor(providerConfig?: ProviderConfig) {
if (providerConfig) {
this.providerHandler = new ProviderOperationHandler(providerConfig);
}
}
/**
* Execute git-pulls operation
*/
async execute(params: GitPullsParams): Promise<ToolResult> {
const startTime = Date.now();
try {
// Validate basic parameters
const validation = ParameterValidator.validateToolParams('git-pulls', 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 = ParameterValidator.validateOperationParams('git-pulls', params.action, params);
if (!operationValidation.isValid) {
return OperationErrorHandler.createToolError(
'VALIDATION_ERROR',
`Operation validation failed: ${operationValidation.errors.join(', ')}`,
params.action,
{ validationErrors: operationValidation.errors },
operationValidation.suggestions
);
}
// Route to appropriate handler
const isRemoteOperation = this.isRemoteOperation(params.action);
if (isRemoteOperation) {
return await this.executeRemoteOperation(params, startTime);
} else {
return await this.executeLocalOperation(params, startTime);
}
} 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']
);
}
}
/**
* Check if operation is remote
*/
private isRemoteOperation(action: string): boolean {
// All pull request operations are remote
const remoteOperations = ['create', 'list', 'get', 'update', 'merge', 'close', 'review', 'search'];
return remoteOperations.includes(action);
}
/**
* Execute remote pull request operations
*/
private async executeRemoteOperation(params: GitPullsParams, startTime: number): Promise<ToolResult> {
// Check provider configuration
if (!this.providerHandler) {
return OperationErrorHandler.createToolError(
'PROVIDER_NOT_CONFIGURED',
'Provider handler is not configured for pull request operations',
params.action,
{},
['Configure GitHub or Gitea provider to use pull request operations']
);
}
if (!params.provider) {
if (configManager.isUniversalMode()) {
params.provider = 'both';
console.error(`[Universal Mode] Auto-applying both providers for ${params.action}`);
} else {
return OperationErrorHandler.createToolError(
'PROVIDER_REQUIRED',
'Provider parameter is required for pull request operations',
params.action,
{},
['Specify provider as: github, gitea, or both']
);
}
}
return await this.executePullRequestOperation(params, startTime);
}
/**
* Execute local pull request operations (if any)
*/
private async executeLocalOperation(params: GitPullsParams, startTime: number): Promise<ToolResult> {
// Pull requests are remote-only operations
return OperationErrorHandler.createToolError(
'LOCAL_OPERATION_NOT_SUPPORTED',
'Pull request operations are not supported locally',
params.action,
{},
['Use remote operations with GitHub or Gitea provider']
);
}
/**
* Execute pull request operation through provider
*/
private async executePullRequestOperation(params: GitPullsParams, startTime: number): Promise<ToolResult> {
const operation: ProviderOperation = {
provider: params.provider!,
operation: this.mapActionToProviderOperation(params.action),
parameters: this.extractPullRequestParameters(params),
requiresAuth: true,
isRemoteOperation: true
};
try {
const result = await this.providerHandler!.executeOperation(operation);
return {
success: result.success,
data: result.partialFailure ? result : result.results[0]?.data,
error: result.success ? undefined : {
code: result.errors[0]?.error?.code || 'PR_OPERATION_ERROR',
message: result.errors[0]?.error?.message || 'Pull request operation failed',
details: result.errors,
suggestions: this.getOperationSuggestions(params.action)
},
metadata: {
provider: params.provider,
operation: params.action,
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'PR_OPERATION_ERROR',
`Pull request operation failed: ${errorMessage}`,
params.action,
{ error: errorMessage },
['Check provider configuration and network connectivity']
);
}
}
/**
* Extract parameters for pull request operations
*/
private extractPullRequestParameters(params: GitPullsParams): Record<string, any> {
const prParams: Record<string, any> = {
projectPath: params.projectPath
};
// Auto-detect repo if not provided
if (params.repo) prParams.repo = params.repo;
// Pull request identification
if (params.pull_number !== undefined) prParams.pull_number = params.pull_number;
// Operation-specific parameters
switch (params.action) {
case 'create':
if (params.title) prParams.title = params.title;
if (params.body) prParams.body = params.body;
if (params.head) prParams.head = params.head;
if (params.base) prParams.base = params.base;
break;
case 'list':
if (params.state_filter) prParams.state = params.state_filter;
if (params.sort) prParams.sort = params.sort;
if (params.direction) prParams.direction = params.direction;
if (params.head_filter) prParams.head = params.head_filter;
if (params.base_filter) prParams.base = params.base_filter;
break;
case 'get':
// Get operations need owner, repo, and pull_number (already handled above)
break;
case 'update':
if (params.title) prParams.title = params.title;
if (params.body) prParams.body = params.body;
if (params.state) prParams.state = params.state;
break;
case 'merge':
if (params.commit_title) prParams.commit_title = params.commit_title;
if (params.commit_message) prParams.commit_message = params.commit_message;
if (params.merge_method) prParams.merge_method = params.merge_method;
break;
case 'close':
prParams.state = 'closed';
break;
case 'review':
if (params.event) prParams.event = params.event;
if (params.review_body) prParams.body = params.review_body;
break;
case 'search':
if (params.query) prParams.query = params.query;
if (params.search_sort) prParams.sort = params.search_sort;
if (params.search_order) prParams.order = params.search_order;
break;
}
return prParams;
}
/**
* Map git-pulls actions to provider operations
*/
private mapActionToProviderOperation(action: string): string {
const actionMap: Record<string, string> = {
'create': 'pr-create',
'list': 'pr-list',
'get': 'pr-get',
'update': 'pr-update',
'merge': 'pr-merge',
'close': 'pr-close',
'review': 'pr-review',
'search': 'pr-search'
};
return actionMap[action] || action;
}
/**
* Get operation-specific suggestions
*/
private getOperationSuggestions(action: string): string[] {
const suggestions: Record<string, string[]> = {
'create': [
'Ensure title, head, and base are provided',
'Check that head branch exists and has commits',
'Verify you have permission to create PRs in the repository',
'Ensure base branch exists and is accessible'
],
'list': [
'Check repository access permissions',
'Verify owner and repo parameters are correct',
'Try different state filters: open, closed, all'
],
'get': [
'Verify the pull request number exists',
'Check repository access permissions',
'Ensure owner and repo parameters are correct'
],
'update': [
'Verify the pull request number exists',
'Check that you have permission to edit the PR',
'Ensure at least one field is being updated'
],
'merge': [
'Verify the pull request number exists and is open',
'Check that you have permission to merge the PR',
'Ensure the PR is mergeable (no conflicts)',
'Check that required status checks are passing'
],
'close': [
'Verify the pull request number exists and is open',
'Check that you have permission to close the PR',
'Ensure the PR is not already closed'
],
'review': [
'Verify the pull request number exists',
'Ensure event is one of: APPROVE, REQUEST_CHANGES, COMMENT',
'Check that you have permission to review the PR',
'Provide review body for REQUEST_CHANGES or COMMENT events'
],
'search': [
'Provide a search query',
'Check search syntax for the provider',
'Try different sort and order parameters'
]
};
return suggestions[action] || ['Check provider configuration and try again'];
}
/**
* Get tool schema for MCP registration
*/
static getToolSchema() {
return {
name: 'git-pulls',
description: 'Comprehensive pull request management tool for Git repositories. Supports create, list, get, update, merge, close, review, and search operations for pull requests. In universal mode (GIT_MCP_MODE=universal), automatically executes on both GitHub and Gitea providers.',
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['create', 'list', 'get', 'update', 'merge', 'close', 'review', 'search'],
description: 'The pull request operation to perform'
},
projectPath: {
type: 'string',
description: 'Absolute path to the project directory'
},
provider: {
type: 'string',
enum: ['github', 'gitea', 'both'],
description: 'Provider for pull request operations (required)'
},
repo: {
type: 'string',
description: 'Repository name (auto-detected if not provided)'
},
pull_number: {
type: 'number',
description: 'Pull request number (required for get/update/merge/close/review actions)'
},
title: {
type: 'string',
description: 'Pull request title (required for create, optional for update)'
},
body: {
type: 'string',
description: 'Pull request body/description'
},
head: {
type: 'string',
description: 'Source branch (required for create)'
},
base: {
type: 'string',
description: 'Target branch (required for create)'
},
state: {
type: 'string',
enum: ['open', 'closed'],
description: 'Pull request state (for update action)'
},
state_filter: {
type: 'string',
enum: ['open', 'closed', 'all'],
description: 'Filter pull requests by state (for list action, default: open)'
},
sort: {
type: 'string',
enum: ['created', 'updated', 'popularity', 'long-running'],
description: 'Sort criteria (for list action, default: created)'
},
direction: {
type: 'string',
enum: ['asc', 'desc'],
description: 'Sort direction (for list action, default: desc)'
},
head_filter: {
type: 'string',
description: 'Filter by head branch (for list action)'
},
base_filter: {
type: 'string',
description: 'Filter by base branch (for list action)'
},
commit_title: {
type: 'string',
description: 'Merge commit title (for merge action)'
},
commit_message: {
type: 'string',
description: 'Merge commit message (for merge action)'
},
merge_method: {
type: 'string',
enum: ['merge', 'squash', 'rebase'],
description: 'Merge method (for merge action, default: merge)'
},
event: {
type: 'string',
enum: ['APPROVE', 'REQUEST_CHANGES', 'COMMENT'],
description: 'Review event (required for review action)'
},
review_body: {
type: 'string',
description: 'Review comment body (for review action)'
},
query: {
type: 'string',
description: 'Search query (required for search action)'
},
search_sort: {
type: 'string',
enum: ['created', 'updated', 'popularity'],
description: 'Sort for search results (default: created)'
},
search_order: {
type: 'string',
enum: ['asc', 'desc'],
description: 'Order for search results (default: desc)'
}
},
required: ['action', 'projectPath']
}
};
}
}