/**
* Response Formatter
*
* Utility for formatting consistent responses across all Git MCP tools.
* Provides standardized success/error response formats with metadata.
*/
export interface ToolResult {
success: boolean;
data?: any;
error?: {
code: string;
message: string;
details?: any;
suggestions?: string[];
configurationGuide?: {
requiredEnvVars: string[];
exampleConfig: any;
};
};
metadata?: {
provider?: string;
operation: string;
timestamp: string;
executionTime: number;
toolName?: string;
projectPath?: string;
repositoryName?: string;
};
}
export interface FormatterOptions {
includeMetadata?: boolean;
includeTimestamp?: boolean;
includeExecutionTime?: boolean;
maxDataSize?: number;
truncateData?: boolean;
}
export class ResponseFormatter {
private defaultOptions: FormatterOptions = {
includeMetadata: true,
includeTimestamp: true,
includeExecutionTime: true,
maxDataSize: 10000, // 10KB
truncateData: true
};
/**
* Format successful response
*/
success(
data: any,
metadata: {
operation: string;
provider?: string;
executionTime?: number;
toolName?: string;
projectPath?: string;
repositoryName?: string;
},
options: FormatterOptions = {}
): ToolResult {
const mergedOptions = { ...this.defaultOptions, ...options };
// Process data size if needed
let processedData = data;
if (mergedOptions.truncateData && mergedOptions.maxDataSize) {
processedData = this.truncateDataIfNeeded(data, mergedOptions.maxDataSize);
}
const result: ToolResult = {
success: true,
data: processedData
};
if (mergedOptions.includeMetadata) {
result.metadata = {
operation: metadata.operation,
timestamp: mergedOptions.includeTimestamp ? new Date().toISOString() : '',
executionTime: mergedOptions.includeExecutionTime ? (metadata.executionTime || 0) : 0
};
if (metadata.provider) result.metadata.provider = metadata.provider;
if (metadata.toolName) result.metadata.toolName = metadata.toolName;
if (metadata.projectPath) result.metadata.projectPath = metadata.projectPath;
if (metadata.repositoryName) result.metadata.repositoryName = metadata.repositoryName;
}
return result;
}
/**
* Format error response
*/
error(
code: string,
message: string,
details?: any,
metadata?: {
operation: string;
provider?: string;
executionTime?: number;
toolName?: string;
projectPath?: string;
repositoryName?: string;
},
options: FormatterOptions = {}
): ToolResult {
const mergedOptions = { ...this.defaultOptions, ...options };
const result: ToolResult = {
success: false,
error: {
code,
message,
details,
suggestions: this.generateSuggestions(code, message, details)
}
};
// Add configuration guide for specific error types
const configGuide = this.generateConfigurationGuide(code, details);
if (configGuide) {
result.error!.configurationGuide = configGuide;
}
if (mergedOptions.includeMetadata && metadata) {
result.metadata = {
operation: metadata.operation,
timestamp: mergedOptions.includeTimestamp ? new Date().toISOString() : '',
executionTime: mergedOptions.includeExecutionTime ? (metadata.executionTime || 0) : 0
};
if (metadata.provider) result.metadata.provider = metadata.provider;
if (metadata.toolName) result.metadata.toolName = metadata.toolName;
if (metadata.projectPath) result.metadata.projectPath = metadata.projectPath;
if (metadata.repositoryName) result.metadata.repositoryName = metadata.repositoryName;
}
return result;
}
/**
* Format validation error response
*/
validationError(
parameter: string,
expectedFormat: string,
actualValue?: any,
metadata?: {
operation: string;
toolName?: string;
}
): ToolResult {
return this.error(
'VALIDATION_ERROR',
`Invalid parameter: ${parameter}`,
{
parameter,
expectedFormat,
actualValue,
example: this.generateParameterExample(parameter, expectedFormat)
},
metadata
);
}
/**
* Format configuration error response
*/
configurationError(
missingConfig: string[],
provider?: string,
metadata?: {
operation: string;
toolName?: string;
}
): ToolResult {
const providerText = provider ? ` for ${provider}` : '';
return this.error(
'CONFIGURATION_ERROR',
`Missing required configuration${providerText}`,
{
missingConfig,
provider
},
metadata
);
}
/**
* Format Git error response
*/
gitError(
gitErrorType: string,
gitMessage: string,
command?: string,
metadata?: {
operation: string;
toolName?: string;
projectPath?: string;
}
): ToolResult {
return this.error(
`GIT_${gitErrorType.toUpperCase()}`,
`Git error: ${gitMessage}`,
{
gitErrorType,
command,
workingDirectory: metadata?.projectPath
},
metadata
);
}
/**
* Format provider error response
*/
providerError(
provider: string,
apiError: string,
statusCode?: number,
metadata?: {
operation: string;
toolName?: string;
}
): ToolResult {
return this.error(
'PROVIDER_ERROR',
`${provider} API error: ${apiError}`,
{
provider,
statusCode,
apiError
},
metadata
);
}
/**
* Format operation not supported error
*/
operationNotSupported(
operation: string,
supportedOperations: string[],
toolName?: string
): ToolResult {
return this.error(
'OPERATION_NOT_SUPPORTED',
`Operation '${operation}' is not supported`,
{
operation,
supportedOperations,
toolName
},
{
operation: 'validate_operation',
toolName
}
);
}
/**
* Format partial success response (for multi-provider operations)
*/
partialSuccess(
successResults: any[],
failedResults: any[],
metadata: {
operation: string;
providers: string[];
executionTime?: number;
toolName?: string;
}
): ToolResult {
const totalProviders = metadata.providers.length;
const successCount = successResults.length;
const failureCount = failedResults.length;
return {
success: successCount > 0, // Partial success if at least one succeeded
data: {
results: successResults,
summary: {
total: totalProviders,
successful: successCount,
failed: failureCount,
successRate: Math.round((successCount / totalProviders) * 100)
}
},
error: failureCount > 0 ? {
code: 'PARTIAL_FAILURE',
message: `${failureCount} out of ${totalProviders} providers failed`,
details: {
failedResults,
failedProviders: failedResults.map((r: any) => r.provider)
},
suggestions: [
'Check failed provider configurations',
'Verify network connectivity',
'Review provider-specific error messages'
]
} : undefined,
metadata: {
operation: metadata.operation,
timestamp: new Date().toISOString(),
executionTime: metadata.executionTime || 0,
toolName: metadata.toolName,
provider: metadata.providers.join(', ')
}
};
}
/**
* Format list response with pagination info
*/
listResponse(
items: any[],
pagination?: {
page?: number;
limit?: number;
total?: number;
hasMore?: boolean;
},
metadata?: {
operation: string;
provider?: string;
executionTime?: number;
toolName?: string;
}
): ToolResult {
const data: any = {
items,
count: items.length
};
if (pagination) {
data.pagination = pagination;
}
return this.success(data, metadata || { operation: 'list' });
}
/**
* Format status response
*/
statusResponse(
status: 'healthy' | 'warning' | 'error',
details: any,
metadata?: {
operation: string;
toolName?: string;
}
): ToolResult {
return this.success(
{
status,
details,
timestamp: new Date().toISOString()
},
metadata || { operation: 'status' }
);
}
/**
* Generate suggestions based on error code and message
*/
private generateSuggestions(code: string, message: string, details?: any): string[] {
const suggestions: string[] = [];
switch (code) {
case 'VALIDATION_ERROR':
suggestions.push('Check parameter format and try again');
suggestions.push('Refer to tool documentation for parameter examples');
break;
case 'CONFIGURATION_ERROR':
suggestions.push('Set required environment variables');
suggestions.push('Check configuration guide in error details');
break;
case 'GIT_NOT_A_REPOSITORY':
suggestions.push('Initialize Git repository with: git init');
suggestions.push('Ensure you are in the correct directory');
break;
case 'GIT_COMMAND_NOT_FOUND':
suggestions.push('Install Git from https://git-scm.com/');
suggestions.push('Ensure Git is in your system PATH');
break;
case 'PROVIDER_ERROR':
suggestions.push('Check API credentials and permissions');
suggestions.push('Verify network connectivity');
if (details?.statusCode === 401) {
suggestions.push('Verify authentication token is valid');
}
if (details?.statusCode === 403) {
suggestions.push('Check repository permissions');
}
if (details?.statusCode === 404) {
suggestions.push('Verify repository exists and is accessible');
}
break;
case 'OPERATION_NOT_SUPPORTED':
suggestions.push('Use one of the supported operations listed in details');
suggestions.push('Check tool documentation for available operations');
break;
default:
suggestions.push('Check error details for more information');
suggestions.push('Retry the operation after addressing the issue');
}
return suggestions;
}
/**
* Generate configuration guide for specific errors
*/
private generateConfigurationGuide(code: string, details?: any): {
requiredEnvVars: string[];
exampleConfig: any;
} | undefined {
if (code === 'CONFIGURATION_ERROR') {
const provider = details?.provider;
if (provider === 'github') {
return {
requiredEnvVars: ['GITHUB_TOKEN', 'GITHUB_USERNAME'],
exampleConfig: {
GITHUB_TOKEN: 'ghp_xxxxxxxxxxxxxxxxxxxx',
GITHUB_USERNAME: 'your-github-username'
}
};
}
if (provider === 'gitea') {
return {
requiredEnvVars: ['GITEA_URL', 'GITEA_TOKEN', 'GITEA_USERNAME'],
exampleConfig: {
GITEA_URL: 'https://gitea.example.com',
GITEA_TOKEN: 'your-gitea-token',
GITEA_USERNAME: 'your-gitea-username'
}
};
}
// Multi-provider configuration
return {
requiredEnvVars: [
'GITHUB_TOKEN', 'GITHUB_USERNAME',
'GITEA_URL', 'GITEA_TOKEN', 'GITEA_USERNAME'
],
exampleConfig: {
github: {
GITHUB_TOKEN: 'ghp_xxxxxxxxxxxxxxxxxxxx',
GITHUB_USERNAME: 'your-github-username'
},
gitea: {
GITEA_URL: 'https://gitea.example.com',
GITEA_TOKEN: 'your-gitea-token',
GITEA_USERNAME: 'your-gitea-username'
}
}
};
}
return undefined;
}
/**
* Generate parameter example based on parameter name and expected format
*/
private generateParameterExample(parameter: string, expectedFormat: string): any {
const examples: Record<string, any> = {
projectPath: '/path/to/your/project',
provider: 'github | gitea | both',
action: 'create | list | get | update | delete',
repositoryName: 'my-repository',
branchName: 'feature/new-feature',
commitMessage: 'Add new feature',
tagName: 'v1.0.0',
issueNumber: 123,
pullRequestNumber: 456,
fileName: 'src/index.ts',
content: 'file content here',
title: 'Issue or PR title',
description: 'Detailed description',
labels: ['bug', 'enhancement'],
assignees: ['username1', 'username2']
};
return examples[parameter] || `<${expectedFormat}>`;
}
/**
* Truncate data if it exceeds maximum size
*/
private truncateDataIfNeeded(data: any, maxSize: number): any {
const dataString = JSON.stringify(data);
if (dataString.length <= maxSize) {
return data;
}
// If data is too large, truncate and add indicator
const truncatedString = dataString.substring(0, maxSize - 100);
try {
// Try to parse truncated JSON (might be invalid)
return JSON.parse(truncatedString);
} catch {
// If truncated JSON is invalid, return summary
return {
_truncated: true,
_originalSize: dataString.length,
_maxSize: maxSize,
_summary: this.generateDataSummary(data),
_note: 'Data was truncated due to size limits'
};
}
}
/**
* Generate summary of data structure
*/
private generateDataSummary(data: any): any {
if (Array.isArray(data)) {
return {
type: 'array',
length: data.length,
sampleItems: data.slice(0, 3),
hasMore: data.length > 3
};
}
if (typeof data === 'object' && data !== null) {
const keys = Object.keys(data);
const summary: any = {
type: 'object',
keys: keys.slice(0, 10),
hasMoreKeys: keys.length > 10
};
// Include sample values for first few keys
for (let i = 0; i < Math.min(3, keys.length); i++) {
const key = keys[i];
summary[key] = typeof data[key] === 'object'
? `[${typeof data[key]}]`
: data[key];
}
return summary;
}
return {
type: typeof data,
value: String(data).substring(0, 100)
};
}
}
// Export singleton instance
export const responseFormatter = new ResponseFormatter();