/**
* Parameter Validation
*
* Provides robust input validation for all Git MCP operations.
* Validates required parameters, formats, and operation support.
*/
import path from 'path';
import { configManager } from '../config.js';
export interface ValidationResult {
isValid: boolean;
errors: string[];
warnings: string[];
suggestions: string[];
}
export interface ToolParams {
action: string;
projectPath: string;
provider?: 'github' | 'gitea' | 'both';
[key: string]: any;
}
export class ParameterValidator {
private static readonly VALID_PROVIDERS = ['github', 'gitea', 'both'];
private static readonly TOOL_OPERATIONS: Record<string, string[]> = {
'git-workflow': ['init', 'commit', 'sync', 'status', 'backup', 'push', 'pull', 'create', 'list', 'get', 'update', 'delete', 'fork', 'search'],
'git-files': ['read', 'search', 'backup', 'list'],
'git-branches': ['create', 'list', 'get', 'delete', 'merge', 'compare'],
'git-issues': ['create', 'list', 'get', 'update', 'close', 'comment', 'search'],
'git-pulls': ['create', 'list', 'get', 'update', 'merge', 'close', 'review', 'search'],
'git-tags': ['create', 'list', 'get', 'delete', 'search'],
'git-release': ['create', 'list', 'get', 'update', 'delete', 'publish', 'download'],
'git-remote': ['add', 'remove', 'rename', 'show', 'set-url', 'prune', 'list'],
'git-reset': ['soft', 'mixed', 'hard', 'reset-to-commit', 'reset-branch'],
'git-stash': ['stash', 'pop', 'apply', 'list', 'show', 'drop', 'clear'],
'git-config': ['get', 'set', 'unset', 'list', 'edit', 'show'],
'git-monitor': ['log', 'status', 'commits', 'contributors'],
'git-backup': ['backup', 'restore', 'list', 'verify'],
'git-archive': ['create', 'extract', 'list', 'verify'],
'git-packages': ['list', 'get', 'create', 'update', 'delete', 'publish', 'download'],
'git-analytics': ['stats', 'commits', 'contributors'],
'git-sync': ['sync', 'status'],
'git-update': ['update', 'history', 'changelog', 'track', 'sync-providers', 'status', 'rollback', 'compare'],
'git-history': ['log', 'track', 'sync', 'export', 'auto']
};
private static readonly REMOTE_OPERATIONS: Record<string, string[]> = {
'git-workflow': ['create', 'list', 'get', 'update', 'delete', 'fork', 'search'],
// git-files operations are local by default, remote only when provider is explicitly specified (read-only only)
'git-files': [],
'git-branches': [], // All branch operations are local for now
'git-issues': ['create', 'list', 'get', 'update', 'close', 'comment', 'search'],
'git-pulls': ['create', 'list', 'get', 'update', 'merge', 'close', 'review', 'search'],
'git-tags': [], // All tag operations are local
'git-release': ['create', 'list', 'get', 'update', 'delete', 'publish', 'download'],
'git-remote': [], // Remote operations are local Git commands
'git-reset': [], // Reset operations are local
'git-stash': [], // Stash operations are local
'git-config': [], // Config operations are local
'git-monitor': [], // Monitor operations are local
'git-backup': [], // Backup operations are local
'git-archive': [], // Archive operations are local
'git-packages': ['create', 'update', 'delete', 'publish', 'download'], // list and get are local
'git-analytics': [], // All analytics operations are local by default
'git-sync': [], // All sync operations are local by default
'git-update': ['sync-providers'], // Only sync-providers is remote
'git-history': ['sync'] // sync operation can be remote when using API method
};
/**
* Validates basic tool parameters
*/
public static validateToolParams(toolName: string, params: ToolParams): ValidationResult {
const result: ValidationResult = {
isValid: true,
errors: [],
warnings: [],
suggestions: []
};
// Validate required parameters
if (!params.projectPath) {
result.errors.push('projectPath is required for all operations');
result.suggestions.push('Provide the absolute path to your project directory');
result.isValid = false;
}
if (!params.action) {
result.errors.push('action is required to specify the operation to perform');
result.suggestions.push(`Valid actions for ${toolName}: ${this.TOOL_OPERATIONS[toolName]?.join(', ') || 'unknown tool'}`);
result.isValid = false;
}
// Validate tool exists
if (!this.TOOL_OPERATIONS[toolName]) {
result.errors.push(`Unknown tool: ${toolName}`);
result.suggestions.push(`Available tools: ${Object.keys(this.TOOL_OPERATIONS).join(', ')}`);
result.isValid = false;
return result;
}
// Validate action is supported by tool
if (params.action && !this.TOOL_OPERATIONS[toolName].includes(params.action)) {
result.errors.push(`Action '${params.action}' is not supported by ${toolName}`);
result.suggestions.push(`Valid actions: ${this.TOOL_OPERATIONS[toolName].join(', ')}`);
result.isValid = false;
}
// Validate provider parameter
if (params.provider && !this.VALID_PROVIDERS.includes(params.provider)) {
result.errors.push(`Invalid provider: ${params.provider}`);
result.suggestions.push(`Valid providers: ${this.VALID_PROVIDERS.join(', ')}`);
result.isValid = false;
}
// Check if provider is required for remote operations
const toolRemoteOps = this.REMOTE_OPERATIONS[toolName] || [];
if (params.action && toolRemoteOps.includes(params.action) && !params.provider) {
// In universal mode, provider is auto-injected, so don't require it
if (!configManager.isUniversalMode()) {
result.errors.push(`Provider is required for remote operation: ${params.action}`);
result.suggestions.push(`Specify provider as one of: ${this.VALID_PROVIDERS.join(', ')}`);
result.isValid = false;
}
}
// Validate projectPath format
if (params.projectPath && typeof params.projectPath !== 'string') {
result.errors.push('projectPath must be a string');
result.isValid = false;
}
// Check for common parameter format issues
if (params.projectPath && params.projectPath.includes('\\') && !params.projectPath.includes(':\\')) {
result.warnings.push('projectPath contains backslashes - ensure it is a valid absolute path');
}
// Validate URLs format
if (params.provider === 'gitea' && params.projectPath) {
// This is a basic check - more specific validation would be in the operation-specific validator
if (params.projectPath.startsWith('http://') || params.projectPath.startsWith('https://')) {
result.warnings.push('projectPath appears to be a URL - ensure you are using the local repository path');
}
}
// Validate branch names format
if (params.action && ['create', 'delete', 'merge'].includes(params.action) && params.branch) {
if (typeof params.branch !== 'string') {
result.errors.push('branch parameter must be a string');
result.isValid = false;
} else {
// Basic branch name validation
if (params.branch.includes('..') || params.branch.includes('~') || params.branch.includes('^')) {
result.warnings.push('branch name contains special characters - ensure it is a valid branch name');
}
if (params.branch.startsWith('-')) {
result.errors.push('branch name cannot start with a hyphen');
result.isValid = false;
}
if (params.branch.includes(' ')) {
result.warnings.push('branch name contains spaces - consider using hyphens or underscores');
}
}
}
// Validate commit hash format
if (params.commit && typeof params.commit === 'string') {
if (params.commit.length < 7 || params.commit.length > 40) {
result.warnings.push('commit hash appears to be invalid format (should be 7-40 characters)');
}
if (!/^[a-f0-9]+$/i.test(params.commit)) {
result.errors.push('commit hash contains invalid characters (only hex digits allowed)');
result.isValid = false;
}
}
// Validate token format (basic check)
if (params.token && typeof params.token === 'string') {
if (params.token.length < 20) {
result.warnings.push('token appears to be too short - ensure it is a valid API token');
}
if (params.token.includes(' ')) {
result.errors.push('token contains spaces - ensure it is a valid token without spaces');
result.isValid = false;
}
}
return result;
}
/**
* Validates specific operation parameters
*/
public static validateOperationParams(toolName: string, action: string, params: ToolParams): ValidationResult {
const result: ValidationResult = {
isValid: true,
errors: [],
warnings: [],
suggestions: []
};
// Tool-specific validations
switch (toolName) {
case 'git-workflow':
return this.validateWorkflowParams(action, params);
case 'git-files':
return this.validateFilesParams(action, params);
case 'git-branches':
return this.validateBranchesParams(action, params);
case 'git-tags':
return this.validateTagsParams(action, params);
case 'git-issues':
return this.validateIssuesParams(action, params);
case 'git-pulls':
return this.validatePullsParams(action, params);
case 'git-sync':
return this.validateSyncParams(action, params);
case 'git-files':
return this.validateFilesParams(action, params);
case 'git-update':
return this.validateUpdateParams(action, params);
default:
// Generic validation for other tools
return result;
}
}
private static validateWorkflowParams(action: string, params: ToolParams): ValidationResult {
const result: ValidationResult = {
isValid: true,
errors: [],
warnings: [],
suggestions: []
};
switch (action) {
case 'commit':
if (!params.message) {
result.errors.push('message is required for commit operation');
result.suggestions.push('Provide a commit message describing your changes');
result.isValid = false;
}
break;
case 'create':
if (!params.name) {
result.errors.push('name is required for repository creation');
result.suggestions.push('Provide a repository name');
result.isValid = false;
}
break;
case 'get':
case 'update':
case 'delete':
case 'fork':
if (!params.repo) {
result.errors.push('repo is required for repository operations');
result.suggestions.push('Provide repository name');
result.isValid = false;
}
break;
case 'search':
if (!params.query) {
result.errors.push('query is required for search operation');
result.suggestions.push('Provide a search query string');
result.isValid = false;
}
break;
case 'push':
// Validate push parameters
if (params.force) {
result.warnings.push('Force push will override remote changes. Use with caution.');
}
break;
case 'pull':
// Validate pull parameters
if (params.strategy && !['merge', 'rebase', 'fast-forward'].includes(params.strategy)) {
result.errors.push('Invalid pull strategy. Must be one of: merge, rebase, fast-forward');
result.suggestions.push('Use strategy: "merge", "rebase", or "fast-forward"');
result.isValid = false;
}
break;
case 'backup':
if (params.backupPath && typeof params.backupPath !== 'string') {
result.errors.push('backupPath must be a string');
result.isValid = false;
}
break;
case 'sync':
if (params.remote && typeof params.remote !== 'string') {
result.errors.push('remote must be a string');
result.isValid = false;
}
if (params.branch && typeof params.branch !== 'string') {
result.errors.push('branch must be a string');
result.isValid = false;
}
break;
}
return result;
}
private static validateBranchesParams(action: string, params: ToolParams): ValidationResult {
const result: ValidationResult = {
isValid: true,
errors: [],
warnings: [],
suggestions: []
};
switch (action) {
case 'create':
if (!params.branchName) {
result.errors.push('branchName is required for branch creation');
result.suggestions.push('Provide a name for the new branch');
result.isValid = false;
}
break;
case 'get':
case 'delete':
case 'merge':
if (!params.branchName) {
result.errors.push('branchName is required for branch operation');
result.suggestions.push('Provide the name of the branch to operate on');
result.isValid = false;
}
break;
case 'compare':
if (!params.baseBranch || !params.compareBranch) {
result.errors.push('baseBranch and compareBranch are required for comparison');
result.suggestions.push('Provide both branch names to compare');
result.isValid = false;
}
break;
case 'list':
// list operation doesn't require any additional parameters
break;
}
return result;
}
private static validateTagsParams(action: string, params: ToolParams): ValidationResult {
const result: ValidationResult = {
isValid: true,
errors: [],
warnings: [],
suggestions: []
};
switch (action) {
case 'create':
if (!params.tagName) {
result.errors.push('tagName is required for tag creation');
result.suggestions.push('Provide a name for the new tag');
result.isValid = false;
}
break;
case 'get':
case 'delete':
if (!params.tagName) {
result.errors.push('tagName is required for tag operation');
result.suggestions.push('Provide the name of the tag to operate on');
result.isValid = false;
}
break;
case 'search':
if (!params.query) {
result.errors.push('query is required for tag search');
result.suggestions.push('Provide a search query to find tags');
result.isValid = false;
}
break;
case 'list':
// list operation doesn't require any additional parameters
break;
}
return result;
}
private static validateIssuesParams(action: string, params: ToolParams): ValidationResult {
const result: ValidationResult = {
isValid: true,
errors: [],
warnings: [],
suggestions: []
};
switch (action) {
case 'create':
if (!params.title) {
result.errors.push('title is required for issue creation');
result.suggestions.push('Provide a descriptive title for the issue');
result.isValid = false;
}
break;
case 'get':
case 'update':
case 'close':
if (params.issue_number === undefined) {
result.errors.push('issue_number is required for issue operations');
result.suggestions.push('Provide the issue number');
result.isValid = false;
}
break;
case 'comment':
if (params.issue_number === undefined) {
result.errors.push('issue_number is required for commenting on issues');
result.suggestions.push('Provide the issue number');
result.isValid = false;
}
if (!params.comment_body) {
result.errors.push('comment_body is required for commenting on issues');
result.suggestions.push('Provide the comment text');
result.isValid = false;
}
break;
case 'search':
if (!params.query) {
result.errors.push('query is required for issue search');
result.suggestions.push('Provide a search query to find issues');
result.isValid = false;
}
break;
}
return result;
}
private static validatePullsParams(action: string, params: ToolParams): ValidationResult {
const result: ValidationResult = {
isValid: true,
errors: [],
warnings: [],
suggestions: []
};
switch (action) {
case 'create':
if (!params.title) {
result.errors.push('title is required for pull request creation');
result.suggestions.push('Provide a descriptive title for the pull request');
result.isValid = false;
}
if (!params.head || !params.base) {
result.errors.push('head and base branches are required for pull request creation');
result.suggestions.push('Specify the source (head) and target (base) branches');
result.isValid = false;
}
break;
case 'get':
case 'update':
case 'merge':
case 'close':
if (params.pull_number === undefined) {
result.errors.push('pull_number is required for pull request operations');
result.suggestions.push('Provide the pull request number');
result.isValid = false;
}
break;
case 'review':
if (params.pull_number === undefined) {
result.errors.push('pull_number is required for reviewing pull requests');
result.suggestions.push('Provide the pull request number');
result.isValid = false;
}
if (!params.event) {
result.errors.push('event is required for pull request reviews');
result.suggestions.push('Specify review event: APPROVE, REQUEST_CHANGES, or COMMENT');
result.isValid = false;
}
if (params.event === 'REQUEST_CHANGES' && !params.review_body) {
result.warnings.push('review_body is recommended for REQUEST_CHANGES reviews');
}
break;
case 'search':
if (!params.query) {
result.errors.push('query is required for pull request search');
result.suggestions.push('Provide a search query to find pull requests');
result.isValid = false;
}
break;
}
return result;
}
/**
* Provides usage examples for correct parameter format
*/
public static getUsageExamples(toolName: string, action?: string): string[] {
const examples: string[] = [];
if (!action) {
examples.push(`${toolName} usage examples:`);
const operations = this.TOOL_OPERATIONS[toolName] || [];
operations.slice(0, 3).forEach(op => {
examples.push(` ${toolName} with action="${op}"`);
});
return examples;
}
switch (toolName) {
case 'git-workflow':
switch (action) {
case 'commit':
examples.push('git-workflow commit: { "action": "commit", "projectPath": "/path/to/project", "message": "Fix bug in authentication" }');
break;
case 'create':
examples.push('git-workflow create: { "action": "create", "projectPath": "/path/to/project", "provider": "github", "name": "my-repo", "description": "My new repository" }');
break;
case 'get':
examples.push('git-workflow get: { "action": "get", "projectPath": "/path/to/project", "provider": "github", "repo": "repository-name" }');
break;
case 'list':
examples.push('git-workflow list: { "action": "list", "projectPath": "/path/to/project", "provider": "github" }');
break;
case 'update':
examples.push('git-workflow update: { "action": "update", "projectPath": "/path/to/project", "provider": "github", "repo": "repository-name", "description": "Updated description" }');
break;
case 'delete':
examples.push('git-workflow delete: { "action": "delete", "projectPath": "/path/to/project", "provider": "github", "repo": "repository-name" }');
break;
case 'fork':
examples.push('git-workflow fork: { "action": "fork", "projectPath": "/path/to/project", "provider": "github", "repo": "repository-name" }');
break;
case 'search':
examples.push('git-workflow search: { "action": "search", "projectPath": "/path/to/project", "provider": "github", "query": "javascript framework" }');
break;
}
break;
case 'git-files':
switch (action) {
case 'read':
examples.push('git-files read: { "action": "read", "projectPath": "/path/to/project", "filePath": "src/index.js" }');
examples.push('git-files read (remote): { "action": "read", "projectPath": "/path/to/project", "provider": "github", "filePath": "README.md", "repo": "repository" }');
break;
case 'create':
examples.push('git-files create: { "action": "create", "projectPath": "/path/to/project", "filePath": "README.md", "content": "# My Project" }');
examples.push('git-files create (remote): { "action": "create", "projectPath": "/path/to/project", "provider": "github", "filePath": "docs/api.md", "content": "# API Documentation", "message": "Add API docs" }');
break;
case 'update':
examples.push('git-files update: { "action": "update", "projectPath": "/path/to/project", "filePath": "package.json", "content": "{\\"name\\": \\"my-package\\"}" }');
examples.push('git-files update (remote): { "action": "update", "projectPath": "/path/to/project", "provider": "github", "filePath": "README.md", "content": "Updated content", "message": "Update README", "sha": "abc123" }');
break;
case 'delete':
examples.push('git-files delete: { "action": "delete", "projectPath": "/path/to/project", "filePath": "temp.txt" }');
examples.push('git-files delete (remote): { "action": "delete", "projectPath": "/path/to/project", "provider": "github", "filePath": "old-file.txt", "message": "Remove old file", "sha": "def456" }');
break;
case 'search':
examples.push('git-files search: { "action": "search", "projectPath": "/path/to/project", "query": "function authenticate" }');
examples.push('git-files search (remote): { "action": "search", "projectPath": "/path/to/project", "provider": "github", "query": "TODO" }');
break;
case 'backup':
examples.push('git-files backup: { "action": "backup", "projectPath": "/path/to/project" }');
examples.push('git-files backup (custom): { "action": "backup", "projectPath": "/path/to/project", "backupPath": "/backup/location", "includePattern": "*.js" }');
break;
}
break;
}
if (examples.length === 0) {
examples.push(`${toolName} ${action}: { "action": "${action}", "projectPath": "/path/to/project" }`);
}
return examples;
}
/**
* Validate git-sync specific parameters
*/
private static validateSyncParams(action: string, params: ToolParams): ValidationResult {
const result: ValidationResult = {
isValid: true,
errors: [],
warnings: [],
suggestions: []
};
switch (action) {
case 'sync':
// Validate sync parameters
if (params.strategy && !['merge', 'rebase', 'fast-forward'].includes(params.strategy)) {
result.errors.push('Invalid sync strategy. Must be one of: merge, rebase, fast-forward');
result.suggestions.push('Use strategy: "merge", "rebase", or "fast-forward"');
result.isValid = false;
}
if (params.force) {
result.warnings.push('Force sync will override uncommitted changes. Use with caution.');
}
break;
case 'status':
// Status operation has no specific validation requirements
break;
}
return result;
}
/**
* Validate git-files specific parameters
*/
private static validateFilesParams(action: string, params: ToolParams): ValidationResult {
const result: ValidationResult = {
isValid: true,
errors: [],
warnings: [],
suggestions: []
};
switch (action) {
case 'read':
if (!params.filePath) {
result.errors.push('filePath is required for read operation');
result.suggestions.push('Provide a filePath parameter with the relative path to the file');
result.isValid = false;
}
if (params.encoding && !['utf8', 'base64', 'binary'].includes(params.encoding)) {
result.errors.push('Invalid encoding. Must be one of: utf8, base64, binary');
result.suggestions.push('Use encoding: "utf8", "base64", or "binary"');
result.isValid = false;
}
break;
case 'search':
if (!params.query) {
result.errors.push('query is required for search operation');
result.suggestions.push('Provide a query parameter with the text to search for');
result.isValid = false;
}
break;
case 'list':
// List operation has no specific validation requirements
break;
case 'backup':
// Backup operation has no specific validation requirements
break;
}
return result;
}
private static validateUpdateParams(action: string, params: ToolParams): ValidationResult {
const result: ValidationResult = {
isValid: true,
errors: [],
warnings: [],
suggestions: []
};
switch (action) {
case 'update':
if (params.updateType && !['all', 'dependencies', 'code', 'docs', 'config'].includes(params.updateType)) {
result.errors.push('Invalid update type. Must be one of: all, dependencies, code, docs, config');
result.suggestions.push('Use updateType: "all", "dependencies", "code", "docs", or "config"');
result.isValid = false;
}
break;
case 'history':
if (params.format && !['json', 'markdown', 'text'].includes(params.format)) {
result.errors.push('Invalid format. Must be one of: json, markdown, text');
result.suggestions.push('Use format: "json", "markdown", or "text"');
result.isValid = false;
}
break;
case 'changelog':
if (params.version && !/^[\d]+\.[\d]+\.[\d]+/.test(params.version)) {
result.warnings.push('Version format should follow semantic versioning (e.g., 1.0.0)');
}
break;
case 'track':
if (!params.trackFile && !params.trackPattern) {
result.warnings.push('No specific file or pattern to track - will track all changes');
}
break;
case 'sync-providers':
if (params.providers && Array.isArray(params.providers)) {
const validProviders = ['github', 'gitea'];
const invalidProviders = params.providers.filter(p => !validProviders.includes(p));
if (invalidProviders.length > 0) {
result.errors.push(`Invalid providers: ${invalidProviders.join(', ')}`);
result.suggestions.push('Use providers: "github", "gitea", or both');
result.isValid = false;
}
}
break;
case 'rollback':
if (!params.rollbackTo) {
result.errors.push('rollbackTo parameter is required for rollback operation');
result.suggestions.push('Provide a commit hash, tag name, or version to rollback to');
result.isValid = false;
}
if (params.rollbackType && !['commit', 'tag', 'version'].includes(params.rollbackType)) {
result.errors.push('Invalid rollback type. Must be one of: commit, tag, version');
result.suggestions.push('Use rollbackType: "commit", "tag", or "version"');
result.isValid = false;
}
break;
case 'compare':
if (!params.compareWith) {
result.errors.push('compareWith parameter is required for compare operation');
result.suggestions.push('Provide a commit hash, tag name, branch, or provider to compare with');
result.isValid = false;
}
if (params.compareType && !['commit', 'tag', 'branch', 'provider'].includes(params.compareType)) {
result.errors.push('Invalid compare type. Must be one of: commit, tag, branch, provider');
result.suggestions.push('Use compareType: "commit", "tag", "branch", or "provider"');
result.isValid = false;
}
break;
case 'status':
// Status operation has no specific validation requirements
break;
}
return result;
}
}