/**
* Git Issues Tool
*
* Comprehensive issue management tool for Git repositories.
* Supports complete issue lifecycle: create, list, get, update, close, comment, search.
* Integrates with GitHub and Gitea providers for remote issue operations.
*/
import { ParameterValidator, ToolParams } from '../utils/parameter-validator.js';
import { OperationErrorHandler, ToolResult } from '../utils/operation-error-handler.js';
import { ProviderOperationHandler } from '../providers/provider-operation-handler.js';
import { ProviderConfig, ProviderOperation } from '../providers/types.js';
import { configManager } from '../config.js';
export interface GitIssuesParams extends ToolParams {
action: 'create' | 'list' | 'get' | 'update' | 'close' | 'comment' | 'search';
// Repository identification
repo?: string; // Repository name (auto-detected if not provided)
// Issue identification
issue_number?: number; // Issue number (for get/update/close/comment)
// Issue creation/update parameters
title?: string; // Issue title (required for create)
body?: string; // Issue body/description
labels?: string[]; // Issue labels
assignees?: string[]; // Issue assignees
milestone?: number; // Milestone number
state?: 'open' | 'closed'; // Issue state (for update)
// Listing parameters
state_filter?: 'open' | 'closed' | 'all'; // Filter issues by state (for list)
sort?: 'created' | 'updated' | 'comments'; // Sort criteria (for list)
direction?: 'asc' | 'desc'; // Sort direction (for list)
since?: string; // Only issues updated after this date (for list)
// Comment parameters
comment_body?: string; // Comment body (required for comment action)
// Search parameters
query?: string; // Search query (for search)
search_sort?: 'created' | 'updated' | 'comments'; // Sort for search results
search_order?: 'asc' | 'desc'; // Order for search results
}
export class GitIssuesTool {
private providerHandler?: ProviderOperationHandler;
constructor(providerConfig?: ProviderConfig) {
if (providerConfig) {
this.providerHandler = new ProviderOperationHandler(providerConfig);
}
}
/**
* Execute git-issues operation
*/
async execute(params: GitIssuesParams): Promise<ToolResult> {
const startTime = Date.now();
try {
// Validate basic parameters
const validation = ParameterValidator.validateToolParams('git-issues', 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-issues', 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 issue operations are remote
const remoteOperations = ['create', 'list', 'get', 'update', 'close', 'comment', 'search'];
return remoteOperations.includes(action);
}
/**
* Execute remote issue operations
*/
private async executeRemoteOperation(params: GitIssuesParams, startTime: number): Promise<ToolResult> {
// Check provider configuration
if (!this.providerHandler) {
return OperationErrorHandler.createToolError(
'PROVIDER_NOT_CONFIGURED',
'Provider handler is not configured for issue operations',
params.action,
{},
['Configure GitHub or Gitea provider to use issue 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 issue operations',
params.action,
{},
['Specify provider as: github, gitea, or both']
);
}
}
return await this.executeIssueOperation(params, startTime);
}
/**
* Execute local issue operations (if any)
*/
private async executeLocalOperation(params: GitIssuesParams, startTime: number): Promise<ToolResult> {
// Issues are remote-only operations
return OperationErrorHandler.createToolError(
'LOCAL_OPERATION_NOT_SUPPORTED',
'Issue operations are not supported locally',
params.action,
{},
['Use remote operations with GitHub or Gitea provider']
);
}
/**
* Execute issue operation through provider
*/
private async executeIssueOperation(params: GitIssuesParams, startTime: number): Promise<ToolResult> {
const operation: ProviderOperation = {
provider: params.provider!,
operation: this.mapActionToProviderOperation(params.action),
parameters: this.extractIssueParameters(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 || 'ISSUE_OPERATION_ERROR',
message: result.errors[0]?.error?.message || 'Issue 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(
'ISSUE_OPERATION_ERROR',
`Issue operation failed: ${errorMessage}`,
params.action,
{ error: errorMessage },
['Check provider configuration and network connectivity']
);
}
}
/**
* Extract parameters for issue operations
*/
private extractIssueParameters(params: GitIssuesParams): Record<string, any> {
const issueParams: Record<string, any> = {
projectPath: params.projectPath
};
// Auto-detect repo if not provided
if (params.repo) issueParams.repo = params.repo;
// Issue identification
if (params.issue_number !== undefined) issueParams.issue_number = params.issue_number;
// Operation-specific parameters
switch (params.action) {
case 'create':
if (params.title) issueParams.title = params.title;
if (params.body) issueParams.body = params.body;
if (params.labels) issueParams.labels = params.labels;
if (params.assignees) issueParams.assignees = params.assignees;
if (params.milestone) issueParams.milestone = params.milestone;
break;
case 'list':
if (params.state_filter) issueParams.state = params.state_filter;
if (params.sort) issueParams.sort = params.sort;
if (params.direction) issueParams.direction = params.direction;
if (params.since) issueParams.since = params.since;
break;
case 'get':
// Get operations need owner, repo, and issue_number (already handled above)
break;
case 'update':
if (params.title) issueParams.title = params.title;
if (params.body) issueParams.body = params.body;
if (params.state) issueParams.state = params.state;
if (params.labels) issueParams.labels = params.labels;
if (params.assignees) issueParams.assignees = params.assignees;
if (params.milestone) issueParams.milestone = params.milestone;
break;
case 'close':
issueParams.state = 'closed';
break;
case 'comment':
if (params.comment_body) issueParams.body = params.comment_body;
break;
case 'search':
if (params.query) issueParams.query = params.query;
if (params.search_sort) issueParams.sort = params.search_sort;
if (params.search_order) issueParams.order = params.search_order;
break;
}
return issueParams;
}
/**
* Map git-issues actions to provider operations
*/
private mapActionToProviderOperation(action: string): string {
const actionMap: Record<string, string> = {
'create': 'issue-create',
'list': 'issue-list',
'get': 'issue-get',
'update': 'issue-update',
'close': 'issue-close',
'comment': 'issue-comment',
'search': 'issue-search'
};
return actionMap[action] || action;
}
/**
* Get operation-specific suggestions
*/
private getOperationSuggestions(action: string): string[] {
const suggestions: Record<string, string[]> = {
'create': [
'Ensure title is provided and not empty',
'Check that you have permission to create issues in the repository',
'Verify repository 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 issue number exists',
'Check repository access permissions',
'Ensure owner and repo parameters are correct'
],
'update': [
'Verify the issue number exists',
'Check that you have permission to edit the issue',
'Ensure at least one field is being updated'
],
'close': [
'Verify the issue number exists and is open',
'Check that you have permission to close the issue',
'Ensure the issue is not already closed'
],
'comment': [
'Verify the issue number exists',
'Ensure comment body is provided and not empty',
'Check that you have permission to comment on the issue'
],
'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-issues',
description: 'Comprehensive issue management tool for Git repositories. Supports create, list, get, update, close, comment, and search operations for issues. 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', 'close', 'comment', 'search'],
description: 'The issue operation to perform'
},
projectPath: {
type: 'string',
description: 'Absolute path to the project directory'
},
provider: {
type: 'string',
enum: ['github', 'gitea', 'both'],
description: 'Provider for issue operations (required)'
},
repo: {
type: 'string',
description: 'Repository name (auto-detected if not provided)'
},
issue_number: {
type: 'number',
description: 'Issue number (required for get/update/close/comment actions)'
},
title: {
type: 'string',
description: 'Issue title (required for create, optional for update)'
},
body: {
type: 'string',
description: 'Issue body/description'
},
labels: {
type: 'array',
items: { type: 'string' },
description: 'Issue labels'
},
assignees: {
type: 'array',
items: { type: 'string' },
description: 'Issue assignees (usernames)'
},
milestone: {
type: 'number',
description: 'Milestone number'
},
state: {
type: 'string',
enum: ['open', 'closed'],
description: 'Issue state (for update action)'
},
state_filter: {
type: 'string',
enum: ['open', 'closed', 'all'],
description: 'Filter issues by state (for list action, default: open)'
},
sort: {
type: 'string',
enum: ['created', 'updated', 'comments'],
description: 'Sort criteria (for list action, default: created)'
},
direction: {
type: 'string',
enum: ['asc', 'desc'],
description: 'Sort direction (for list action, default: desc)'
},
since: {
type: 'string',
description: 'Only issues updated after this date (ISO 8601 format, for list action)'
},
comment_body: {
type: 'string',
description: 'Comment body (required for comment action)'
},
query: {
type: 'string',
description: 'Search query (required for search action)'
},
search_sort: {
type: 'string',
enum: ['created', 'updated', 'comments'],
description: 'Sort for search results (default: created)'
},
search_order: {
type: 'string',
enum: ['asc', 'desc'],
description: 'Order for search results (default: desc)'
}
},
required: ['action', 'projectPath']
}
};
}
}