# Directory Structure
```
├── .gitignore
├── jest.config.js
├── package.json
├── README.md
├── src
│ ├── caching
│ │ ├── cache.ts
│ │ └── repository-cache.ts
│ ├── common
│ │ └── command-builder.ts
│ ├── errors
│ │ ├── error-handler.ts
│ │ └── error-types.ts
│ ├── git-operations.ts
│ ├── index.ts
│ ├── monitoring
│ │ ├── performance.ts
│ │ └── types.ts
│ ├── operations
│ │ ├── base
│ │ │ ├── base-operation.ts
│ │ │ └── operation-result.ts
│ │ ├── branch
│ │ │ ├── branch-operations.ts
│ │ │ └── branch-types.ts
│ │ ├── remote
│ │ │ ├── remote-operations.ts
│ │ │ └── remote-types.ts
│ │ ├── repository
│ │ │ └── repository-operations.ts
│ │ ├── sync
│ │ │ ├── sync-operations.ts
│ │ │ └── sync-types.ts
│ │ ├── tag
│ │ │ ├── tag-operations.ts
│ │ │ └── tag-types.ts
│ │ └── working-tree
│ │ ├── working-tree-operations.ts
│ │ └── working-tree-types.ts
│ ├── tool-handler.ts
│ ├── types.ts
│ └── utils
│ ├── command.ts
│ ├── logger.ts
│ ├── path.ts
│ ├── paths.ts
│ └── repository.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependencies
node_modules/
package-lock.json
# Build output
build/
dist/
*.tsbuildinfo
# Environment variables
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# System files
.DS_Store
Thumbs.db
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Git MCP Server
A Model Context Protocol (MCP) server that provides enhanced Git operations through a standardized interface. This server integrates with the MCP ecosystem to provide Git functionality to AI assistants.
## Features
- **Core Git Operations**: init, clone, status, add, commit, push, pull
- **Branch Management**: list, create, delete, checkout
- **Tag Operations**: list, create, delete
- **Remote Management**: list, add, remove
- **Stash Operations**: list, save, pop
- **Bulk Actions**: Execute multiple Git operations in sequence
- **GitHub Integration**: Built-in GitHub support via Personal Access Token
- **Path Resolution**: Smart path handling with optional default path configuration
- **Error Handling**: Comprehensive error handling with custom error types
- **Repository Caching**: Efficient repository state management
- **Performance Monitoring**: Built-in performance tracking
## Installation
1. Clone the repository:
```bash
git clone https://github.com/yourusername/git-mcp-v2.git
cd git-mcp-v2
```
2. Install dependencies:
```bash
npm install
```
3. Build the project:
```bash
npm run build
```
## Configuration
Add to your MCP settings file:
```json
{
"mcpServers": {
"git-v2": {
"command": "node",
"args": ["path/to/git-mcp-v2/build/index.js"],
"env": {
"GIT_DEFAULT_PATH": "/path/to/default/git/directory",
"GITHUB_PERSONAL_ACCESS_TOKEN": "your-github-pat"
}
}
}
}
```
## Environment Variables
- `GIT_DEFAULT_PATH`: (Optional) Default path for Git operations
- `GITHUB_PERSONAL_ACCESS_TOKEN`: (Optional) GitHub Personal Access Token for GitHub operations
## Available Tools
### Basic Operations
- `init`: Initialize a new Git repository
- `clone`: Clone a repository
- `status`: Get repository status
- `add`: Stage files
- `commit`: Create a commit
- `push`: Push commits to remote
- `pull`: Pull changes from remote
### Branch Operations
- `branch_list`: List all branches
- `branch_create`: Create a new branch
- `branch_delete`: Delete a branch
- `checkout`: Switch branches or restore working tree files
### Tag Operations
- `tag_list`: List tags
- `tag_create`: Create a tag
- `tag_delete`: Delete a tag
### Remote Operations
- `remote_list`: List remotes
- `remote_add`: Add a remote
- `remote_remove`: Remove a remote
### Stash Operations
- `stash_list`: List stashes
- `stash_save`: Save changes to stash
- `stash_pop`: Apply and remove a stash
### Bulk Operations
- `bulk_action`: Execute multiple Git operations in sequence
## Development
```bash
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Run linter
npm run lint
# Format code
npm run format
```
## License
MIT
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
/** @type {import('ts-jest').JestConfigWithTsJest} */
export default {
preset: 'ts-jest',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/index.ts',
'!src/**/*.types.ts'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
testMatch: [
'<rootDir>/tests/**/*.test.ts'
],
setupFilesAfterEnv: [
'<rootDir>/tests/setup.ts'
],
testPathIgnorePatterns: [
'/node_modules/',
'/build/'
],
verbose: true,
testTimeout: 10000
};
```
--------------------------------------------------------------------------------
/src/monitoring/types.ts:
--------------------------------------------------------------------------------
```typescript
import { GitMcpError } from '../errors/error-types.js';
import { ErrorCategory, ErrorSeverity } from '../errors/error-types.js';
import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
/**
* Performance monitoring types
*/
/**
* Performance error context
*/
export interface PerformanceErrorContext {
currentUsage?: number;
threshold?: number;
operation?: string;
details?: Record<string, any>;
[key: string]: unknown; // Index signature for additional properties
}
/**
* Performance error with context
*/
export class PerformanceError extends GitMcpError {
constructor(
message: string,
context: PerformanceErrorContext
) {
super(
ErrorCode.InternalError,
message,
ErrorSeverity.HIGH,
ErrorCategory.SYSTEM,
{
operation: context.operation || 'performance',
timestamp: Date.now(),
severity: ErrorSeverity.HIGH,
category: ErrorCategory.SYSTEM,
details: context
}
);
this.name = 'PerformanceError';
}
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "git-mcp-server",
"version": "1.0.0",
"description": "A Model Context Protocol server",
"private": true,
"type": "module",
"bin": {
"git-mcp-server": "./build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"prepare": "npm run build",
"watch": "tsc --watch",
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src --ext .ts",
"format": "prettier --write \"src/**/*.ts\"",
"clean": "rimraf build coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.0.4",
"simple-git": "^3.27.0"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"@types/node": "^20.17.10",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^27.6.3",
"jest": "^29.7.0",
"prettier": "^3.2.4",
"rimraf": "^5.0.5",
"ts-jest": "^29.1.1",
"typescript": "^5.3.3"
}
}
```
--------------------------------------------------------------------------------
/src/operations/base/operation-result.ts:
--------------------------------------------------------------------------------
```typescript
import { GitToolContent } from '../../types.js';
import { GitMcpError } from '../../errors/error-types.js';
/**
* Represents the result of a Git operation with proper type safety
*/
export interface GitOperationResult<T = void> {
/** Whether the operation was successful */
success: boolean;
/** Operation-specific data if successful */
data?: T;
/** Error information if operation failed */
error?: GitMcpError;
/** Standard MCP tool response content */
content: GitToolContent[];
/** Additional metadata about the operation */
meta?: Record<string, unknown>;
}
/**
* Base interface for all Git operation options
*/
export interface GitOperationOptions {
/** Operation path override */
path?: string;
/** Whether to use caching */
useCache?: boolean;
/** Whether to invalidate cache after operation */
invalidateCache?: boolean;
}
/**
* Common result types for Git operations
*/
import { CommandResult as BaseCommandResult } from '../../utils/command.js';
export interface CommandResult extends BaseCommandResult {
// Extend the base command result with any additional fields we need
}
export interface ListResult {
items: string[];
raw: string;
}
export interface StatusResult {
staged: string[];
unstaged: string[];
untracked: string[];
raw: string;
}
export interface BranchResult {
current: string;
branches: string[];
raw: string;
}
export interface TagResult {
tags: string[];
raw: string;
}
export interface RemoteResult {
remotes: Array<{
name: string;
url: string;
purpose: 'fetch' | 'push';
}>;
raw: string;
}
export interface StashResult {
stashes: Array<{
index: number;
message: string;
}>;
raw: string;
}
```
--------------------------------------------------------------------------------
/src/operations/tag/tag-types.ts:
--------------------------------------------------------------------------------
```typescript
import { GitOperationOptions } from '../base/operation-result.js';
/**
* Options for listing tags
*/
export interface TagListOptions extends GitOperationOptions {
/** Show tag message */
showMessage?: boolean;
/** Sort tags by specific key */
sort?: 'version' | 'creatordate' | 'taggerdate';
/** Show only tags containing the specified commit */
contains?: string;
/** Match tags with pattern */
pattern?: string;
}
/**
* Options for creating tags
*/
export interface TagCreateOptions extends GitOperationOptions {
/** Name of the tag to create */
name: string;
/** Tag message (creates annotated tag) */
message?: string;
/** Whether to force create even if tag exists */
force?: boolean;
/** Create a signed tag */
sign?: boolean;
/** Specific commit to tag */
commit?: string;
}
/**
* Options for deleting tags
*/
export interface TagDeleteOptions extends GitOperationOptions {
/** Name of the tag to delete */
name: string;
/** Whether to force delete */
force?: boolean;
/** Also delete the tag from remotes */
remote?: boolean;
}
/**
* Structured tag information
*/
export interface TagInfo {
/** Tag name */
name: string;
/** Whether this is an annotated tag */
annotated: boolean;
/** Tag message if annotated */
message?: string;
/** Tagger information if annotated */
tagger?: {
name: string;
email: string;
date: string;
};
/** Commit that is tagged */
commit: string;
/** Whether this is a signed tag */
signed: boolean;
}
/**
* Result of tag listing operation
*/
export interface TagListResult {
/** List of all tags */
tags: TagInfo[];
/** Raw command output */
raw: string;
}
/**
* Result of tag creation operation
*/
export interface TagCreateResult {
/** Name of created tag */
name: string;
/** Whether it's an annotated tag */
annotated: boolean;
/** Whether it's signed */
signed: boolean;
/** Tagged commit */
commit?: string;
/** Raw command output */
raw: string;
}
/**
* Result of tag deletion operation
*/
export interface TagDeleteResult {
/** Name of deleted tag */
name: string;
/** Whether it was force deleted */
forced: boolean;
/** Raw command output */
raw: string;
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { ToolHandler } from './tool-handler.js';
import { logger } from './utils/logger.js';
import { CommandExecutor } from './utils/command.js';
import { PathResolver } from './utils/paths.js';
async function validateDefaultPath(): Promise<void> {
const defaultPath = process.env.GIT_DEFAULT_PATH;
if (!defaultPath) {
logger.warn('startup', 'GIT_DEFAULT_PATH not set - absolute paths will be required for all operations');
return;
}
try {
// Validate the default path exists and is accessible
PathResolver.validatePath(defaultPath, 'startup', {
mustExist: true,
mustBeDirectory: true,
createIfMissing: true
});
logger.info('startup', 'Default git path validated', defaultPath);
} catch (error) {
logger.error('startup', 'Invalid GIT_DEFAULT_PATH', defaultPath, error as Error);
throw new McpError(
ErrorCode.InternalError,
`Invalid GIT_DEFAULT_PATH: ${(error as Error).message}`
);
}
}
async function main() {
try {
// Validate git installation first
await CommandExecutor.validateGitInstallation('startup');
logger.info('startup', 'Git installation validated');
// Validate default path if provided
await validateDefaultPath();
// Create and configure server
const server = new Server(
{
name: 'git-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Set up error handling
server.onerror = (error) => {
if (error instanceof McpError) {
logger.error('server', error.message, undefined, error);
} else {
logger.error('server', 'Unexpected error', undefined, error as Error);
}
};
// Initialize tool handler
new ToolHandler(server);
// Connect server
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info('server', 'Git MCP server running on stdio');
// Handle shutdown
process.on('SIGINT', async () => {
logger.info('server', 'Shutting down server');
await server.close();
process.exit(0);
});
} catch (error) {
logger.error('startup', 'Failed to start server', undefined, error as Error);
process.exit(1);
}
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/operations/remote/remote-types.ts:
--------------------------------------------------------------------------------
```typescript
import { GitOperationOptions } from '../base/operation-result.js';
/**
* Options for listing remotes
*/
export interface RemoteListOptions extends GitOperationOptions {
/** Show remote URLs */
verbose?: boolean;
}
/**
* Options for adding remotes
*/
export interface RemoteAddOptions extends GitOperationOptions {
/** Name of the remote */
name: string;
/** URL of the remote */
url: string;
/** Whether to fetch immediately */
fetch?: boolean;
/** Tags to fetch (--tags, --no-tags) */
tags?: boolean;
/** Mirror mode (--mirror=fetch or --mirror=push) */
mirror?: 'fetch' | 'push';
}
/**
* Options for removing remotes
*/
export interface RemoteRemoveOptions extends GitOperationOptions {
/** Name of the remote */
name: string;
}
/**
* Options for updating remote URLs
*/
export interface RemoteSetUrlOptions extends GitOperationOptions {
/** Name of the remote */
name: string;
/** New URL for the remote */
url: string;
/** Whether this is a push URL */
pushUrl?: boolean;
/** Add URL instead of changing existing URLs */
add?: boolean;
/** Delete URL instead of changing it */
delete?: boolean;
}
/**
* Options for pruning remotes
*/
export interface RemotePruneOptions extends GitOperationOptions {
/** Name of the remote */
name: string;
/** Whether to show what would be done */
dryRun?: boolean;
}
/**
* Represents a remote configuration
*/
export interface RemoteConfig {
/** Remote name */
name: string;
/** Fetch URL */
fetchUrl: string;
/** Push URL (if different from fetch) */
pushUrl?: string;
/** Remote branches tracked */
branches?: string[];
/** Whether tags are fetched */
fetchTags?: boolean;
/** Mirror configuration */
mirror?: 'fetch' | 'push';
}
/**
* Result of remote listing operation
*/
export interface RemoteListResult {
/** List of remotes */
remotes: RemoteConfig[];
/** Raw command output */
raw: string;
}
/**
* Result of remote add operation
*/
export interface RemoteAddResult {
/** Added remote configuration */
remote: RemoteConfig;
/** Raw command output */
raw: string;
}
/**
* Result of remote remove operation
*/
export interface RemoteRemoveResult {
/** Name of removed remote */
name: string;
/** Raw command output */
raw: string;
}
/**
* Result of remote set-url operation
*/
export interface RemoteSetUrlResult {
/** Updated remote configuration */
remote: RemoteConfig;
/** Raw command output */
raw: string;
}
/**
* Result of remote prune operation
*/
export interface RemotePruneResult {
/** Name of pruned remote */
name: string;
/** Branches that were pruned */
prunedBranches: string[];
/** Raw command output */
raw: string;
}
```
--------------------------------------------------------------------------------
/src/operations/working-tree/working-tree-types.ts:
--------------------------------------------------------------------------------
```typescript
import { GitOperationOptions } from '../base/operation-result.js';
/**
* Options for adding files to staging
*/
export interface AddOptions extends GitOperationOptions {
/** Files to stage */
files: string[];
/** Whether to add all files (including untracked) */
all?: boolean;
/** Whether to add only updates to already tracked files */
update?: boolean;
/** Whether to ignore removal of files */
ignoreRemoval?: boolean;
/** Whether to add files with errors */
force?: boolean;
/** Whether to only show what would be added */
dryRun?: boolean;
}
/**
* Options for committing changes
*/
export interface CommitOptions extends GitOperationOptions {
/** Commit message */
message: string;
/** Whether to allow empty commits */
allowEmpty?: boolean;
/** Whether to amend the previous commit */
amend?: boolean;
/** Whether to skip pre-commit hooks */
noVerify?: boolean;
/** Author of the commit (in format: "Name <email>") */
author?: string;
/** Files to commit (if not specified, commits all staged changes) */
files?: string[];
}
/**
* Options for checking status
*/
export interface StatusOptions extends GitOperationOptions {
/** Whether to show untracked files */
showUntracked?: boolean;
/** Whether to ignore submodules */
ignoreSubmodules?: boolean;
/** Whether to show ignored files */
showIgnored?: boolean;
/** Whether to show branch info */
showBranch?: boolean;
}
/**
* Represents a file change in the working tree
*/
export interface FileChange {
/** Path of the file */
path: string;
/** Type of change */
type: 'added' | 'modified' | 'deleted' | 'renamed' | 'copied' | 'untracked' | 'ignored';
/** Original path for renamed files */
originalPath?: string;
/** Whether the change is staged */
staged: boolean;
/** Raw status code from Git */
raw: string;
}
/**
* Result of add operation
*/
export interface AddResult {
/** Files that were staged */
staged: string[];
/** Files that were not staged (with reasons) */
notStaged?: Array<{
path: string;
reason: string;
}>;
/** Raw command output */
raw: string;
}
/**
* Result of commit operation
*/
export interface CommitResult {
/** Commit hash */
hash: string;
/** Number of files changed */
filesChanged: number;
/** Number of insertions */
insertions: number;
/** Number of deletions */
deletions: number;
/** Whether it was an amend */
amended: boolean;
/** Raw command output */
raw: string;
}
/**
* Result of status operation
*/
export interface StatusResult {
/** Current branch name */
branch: string;
/** Whether the working tree is clean */
clean: boolean;
/** Staged changes */
staged: FileChange[];
/** Unstaged changes */
unstaged: FileChange[];
/** Untracked files */
untracked: FileChange[];
/** Ignored files (if requested) */
ignored?: FileChange[];
/** Raw command output */
raw: string;
}
```
--------------------------------------------------------------------------------
/src/operations/branch/branch-types.ts:
--------------------------------------------------------------------------------
```typescript
import { GitOperationOptions } from '../base/operation-result.js';
/**
* Options for listing branches
*/
export interface BranchListOptions extends GitOperationOptions {
/** Show remote branches */
remotes?: boolean;
/** Show all branches (local and remote) */
all?: boolean;
/** Show only branches containing the specified commit */
contains?: string;
/** Show only branches merged into the specified commit */
merged?: string;
/** Show only branches not merged into the specified commit */
noMerged?: string;
}
/**
* Options for creating branches
*/
export interface BranchCreateOptions extends GitOperationOptions {
/** Name of the branch to create */
name: string;
/** Whether to force create even if branch exists */
force?: boolean;
/** Set up tracking mode (true = --track, false = --no-track) */
track?: boolean;
/** Set upstream for push/pull */
setUpstream?: boolean;
/** Start point (commit/branch) for the new branch */
startPoint?: string;
}
/**
* Options for deleting branches
*/
export interface BranchDeleteOptions extends GitOperationOptions {
/** Name of the branch to delete */
name: string;
/** Whether to force delete even if not merged */
force?: boolean;
/** Also delete the branch from remotes */
remote?: boolean;
}
/**
* Options for checking out branches
*/
export interface CheckoutOptions extends GitOperationOptions {
/** Branch/commit/tag to check out */
target: string;
/** Whether to force checkout even with local changes */
force?: boolean;
/** Create a new branch and check it out */
newBranch?: string;
/** Track the remote branch */
track?: boolean;
}
/**
* Structured branch information
*/
export interface BranchInfo {
/** Branch name */
name: string;
/** Whether this is the current branch */
current: boolean;
/** Remote tracking branch if any */
tracking?: string;
/** Whether the branch is ahead/behind tracking branch */
status?: {
ahead: number;
behind: number;
};
/** Whether this is a remote branch */
remote: boolean;
/** Latest commit hash */
commit?: string;
/** Latest commit message */
message?: string;
}
/**
* Result of branch listing operation
*/
export interface BranchListResult {
/** Current branch name */
current: string;
/** List of all branches */
branches: BranchInfo[];
/** Raw command output */
raw: string;
}
/**
* Result of branch creation operation
*/
export interface BranchCreateResult {
/** Name of created branch */
name: string;
/** Starting point of the branch */
startPoint?: string;
/** Whether tracking was set up */
tracking?: string;
/** Raw command output */
raw: string;
}
/**
* Result of branch deletion operation
*/
export interface BranchDeleteResult {
/** Name of deleted branch */
name: string;
/** Whether it was force deleted */
forced: boolean;
/** Raw command output */
raw: string;
}
/**
* Result of checkout operation
*/
export interface CheckoutResult {
/** Target that was checked out */
target: string;
/** Whether a new branch was created */
newBranch?: string;
/** Previous HEAD position */
previousHead?: string;
/** Raw command output */
raw: string;
}
```
--------------------------------------------------------------------------------
/src/operations/sync/sync-types.ts:
--------------------------------------------------------------------------------
```typescript
import { GitOperationOptions } from '../base/operation-result.js';
/**
* Options for push operations
*/
export interface PushOptions extends GitOperationOptions {
/** Remote to push to */
remote?: string;
/** Branch to push */
branch: string;
/** Whether to force push */
force?: boolean;
/** Whether to force push with lease */
forceWithLease?: boolean;
/** Whether to push all branches */
all?: boolean;
/** Whether to push tags */
tags?: boolean;
/** Whether to skip pre-push hooks */
noVerify?: boolean;
/** Whether to set upstream for branch */
setUpstream?: boolean;
/** Whether to delete remote branches that were deleted locally */
prune?: boolean;
}
/**
* Options for pull operations
*/
export interface PullOptions extends GitOperationOptions {
/** Remote to pull from */
remote?: string;
/** Branch to pull */
branch: string;
/** Whether to rebase instead of merge */
rebase?: boolean;
/** Whether to automatically stash/unstash changes */
autoStash?: boolean;
/** Whether to allow unrelated histories */
allowUnrelated?: boolean;
/** Whether to fast-forward only */
ff?: 'only' | 'no' | true;
/** Strategy to use when merging */
strategy?: 'recursive' | 'resolve' | 'octopus' | 'ours' | 'subtree';
/** Strategy options */
strategyOption?: string[];
}
/**
* Options for fetch operations
*/
export interface FetchOptions extends GitOperationOptions {
/** Remote to fetch from */
remote?: string;
/** Whether to fetch all remotes */
all?: boolean;
/** Whether to prune remote branches */
prune?: boolean;
/** Whether to prune tags */
pruneTags?: boolean;
/** Whether to fetch tags */
tags?: boolean;
/** Whether to fetch only tags */
tagsOnly?: boolean;
/** Whether to force fetch tags */
forceTags?: boolean;
/** Depth of history to fetch */
depth?: number;
/** Whether to update submodules */
recurseSubmodules?: boolean | 'on-demand';
/** Whether to show progress */
progress?: boolean;
}
/**
* Result of push operation
*/
export interface PushResult {
/** Remote that was pushed to */
remote: string;
/** Branch that was pushed */
branch: string;
/** Whether force push was used */
forced: boolean;
/** New remote ref */
newRef?: string;
/** Old remote ref */
oldRef?: string;
/** Summary of changes */
summary: {
created?: string[];
deleted?: string[];
updated?: string[];
rejected?: string[];
};
/** Raw command output */
raw: string;
}
/**
* Result of pull operation
*/
export interface PullResult {
/** Remote that was pulled from */
remote: string;
/** Branch that was pulled */
branch: string;
/** Whether rebase was used */
rebased: boolean;
/** Files changed */
filesChanged: number;
/** Number of insertions */
insertions: number;
/** Number of deletions */
deletions: number;
/** Summary of changes */
summary: {
merged?: string[];
conflicts?: string[];
};
/** Raw command output */
raw: string;
}
/**
* Result of fetch operation
*/
export interface FetchResult {
/** Remote that was fetched from */
remote?: string;
/** Summary of changes */
summary: {
branches?: Array<{
name: string;
oldRef?: string;
newRef: string;
}>;
tags?: Array<{
name: string;
oldRef?: string;
newRef: string;
}>;
pruned?: string[];
};
/** Raw command output */
raw: string;
}
```
--------------------------------------------------------------------------------
/src/operations/repository/repository-operations.ts:
--------------------------------------------------------------------------------
```typescript
import { BaseGitOperation } from '../base/base-operation.js';
import { GitOperationOptions, CommandResult } from '../base/operation-result.js';
import { GitCommandBuilder } from '../../common/command-builder.js';
import { ErrorHandler } from '../../errors/error-handler.js';
import { PathValidator } from '../../utils/path.js';
/**
* Options for repository initialization
*/
export interface InitOptions extends GitOperationOptions {
/** Whether to create a bare repository */
bare?: boolean;
/** Initial branch name */
initialBranch?: string;
}
/**
* Options for repository cloning
*/
export interface CloneOptions extends GitOperationOptions {
/** Repository URL to clone from */
url: string;
/** Whether to create a bare repository */
bare?: boolean;
/** Depth of history to clone */
depth?: number;
/** Branch to clone */
branch?: string;
}
/**
* Handles Git repository initialization
*/
export class InitOperation extends BaseGitOperation<InitOptions> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.init();
if (this.options.bare) {
command.flag('bare');
}
if (this.options.initialBranch) {
command.option('initial-branch', this.options.initialBranch);
}
return command;
}
protected parseResult(result: CommandResult): void {
// Init doesn't return any structured data
}
protected getCacheConfig() {
return {
command: 'init'
};
}
protected validateOptions(): void {
const path = this.options.path || process.env.GIT_DEFAULT_PATH;
if (!path) {
throw ErrorHandler.handleValidationError(
new Error('Path must be provided when GIT_DEFAULT_PATH is not set'),
{ operation: this.context.operation }
);
}
// Validate path exists or can be created
PathValidator.validatePath(path, {
mustExist: false,
allowDirectory: true
});
}
}
/**
* Handles Git repository cloning
*/
export class CloneOperation extends BaseGitOperation<CloneOptions> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.clone()
.arg(this.options.url)
.arg(this.options.path || '.');
if (this.options.bare) {
command.flag('bare');
}
if (this.options.depth) {
command.option('depth', this.options.depth.toString());
}
if (this.options.branch) {
command.option('branch', this.options.branch);
}
return command;
}
protected parseResult(result: CommandResult): void {
// Clone doesn't return any structured data
}
protected getCacheConfig() {
return {
command: 'clone'
};
}
protected validateOptions(): void {
if (!this.options.url) {
throw ErrorHandler.handleValidationError(
new Error('URL is required for clone operation'),
{ operation: this.context.operation }
);
}
const path = this.options.path || process.env.GIT_DEFAULT_PATH;
if (!path) {
throw ErrorHandler.handleValidationError(
new Error('Path must be provided when GIT_DEFAULT_PATH is not set'),
{ operation: this.context.operation }
);
}
// Validate path exists or can be created
PathValidator.validatePath(path, {
mustExist: false,
allowDirectory: true
});
if (this.options.depth !== undefined && this.options.depth <= 0) {
throw ErrorHandler.handleValidationError(
new Error('Depth must be a positive number'),
{ operation: this.context.operation }
);
}
}
}
```
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
```typescript
import { McpError } from '@modelcontextprotocol/sdk/types.js';
import { resolve, relative } from 'path';
export enum LogLevel {
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR'
}
interface LogEntry {
timestamp: string;
level: LogLevel;
operation: string;
message: string;
path?: string;
error?: Error;
context?: Record<string, any>;
}
export class Logger {
private static instance: Logger;
private entries: LogEntry[] = [];
private readonly cwd: string;
private constructor() {
this.cwd = process.cwd();
}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
private formatPath(path: string): string {
const absolutePath = resolve(this.cwd, path);
return relative(this.cwd, absolutePath);
}
private createEntry(
level: LogLevel,
operation: string,
message: string,
path?: string,
error?: Error,
context?: Record<string, any>
): LogEntry {
return {
timestamp: new Date().toISOString(),
level,
operation,
message,
path: path ? this.formatPath(path) : undefined,
error,
context,
};
}
private log(entry: LogEntry): void {
this.entries.push(entry);
let logMessage = `[${entry.timestamp}] ${entry.level} - ${entry.operation}: ${entry.message}`;
if (entry.path) {
logMessage += `\n Path: ${entry.path}`;
}
if (entry.context) {
logMessage += `\n Context: ${JSON.stringify(entry.context, null, 2)}`;
}
if (entry.error) {
if (entry.error instanceof McpError) {
logMessage += `\n Error: ${entry.error.message}`;
} else {
logMessage += `\n Error: ${entry.error.stack || entry.error.message}`;
}
}
console.error(logMessage);
}
debug(operation: string, message: string, path?: string, context?: Record<string, any>): void {
this.log(this.createEntry(LogLevel.DEBUG, operation, message, path, undefined, context));
}
info(operation: string, message: string, path?: string, context?: Record<string, any>): void {
this.log(this.createEntry(LogLevel.INFO, operation, message, path, undefined, context));
}
warn(operation: string, message: string, path?: string, error?: Error, context?: Record<string, any>): void {
this.log(this.createEntry(LogLevel.WARN, operation, message, path, error, context));
}
error(operation: string, message: string, path?: string, error?: Error, context?: Record<string, any>): void {
this.log(this.createEntry(LogLevel.ERROR, operation, message, path, error, context));
}
getEntries(): LogEntry[] {
return [...this.entries];
}
getEntriesForOperation(operation: string): LogEntry[] {
return this.entries.filter(entry => entry.operation === operation);
}
getEntriesForPath(path: string): LogEntry[] {
const searchPath = this.formatPath(path);
return this.entries.filter(entry => entry.path === searchPath);
}
clear(): void {
this.entries = [];
}
// Helper methods for common operations
logCommand(operation: string, command: string, path?: string, context?: Record<string, any>): void {
this.debug(operation, `Executing command: ${command}`, path, context);
}
logCommandResult(operation: string, result: string, path?: string, context?: Record<string, any>): void {
this.debug(operation, `Command result: ${result}`, path, context);
}
logPathValidation(operation: string, path: string, context?: Record<string, any>): void {
this.debug(operation, `Validating path: ${path}`, path, context);
}
logGitOperation(operation: string, details: string, path?: string, context?: Record<string, any>): void {
this.info(operation, details, path, context);
}
logError(operation: string, error: Error, path?: string, context?: Record<string, any>): void {
this.error(operation, 'Operation failed', path, error, context);
}
}
// Export a singleton instance
export const logger = Logger.getInstance();
```
--------------------------------------------------------------------------------
/src/common/command-builder.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Provides a fluent interface for building Git commands with proper option handling
*/
export class GitCommandBuilder {
private command: string[] = ['git'];
private options: Map<string, string | boolean> = new Map();
/**
* Create a new GitCommandBuilder for a specific Git command
*/
constructor(command: string) {
this.command.push(command);
}
/**
* Add a positional argument to the command
*/
arg(value: string): this {
this.command.push(this.escapeArg(value));
return this;
}
/**
* Add multiple positional arguments
*/
args(...values: string[]): this {
values.forEach(value => this.arg(value));
return this;
}
/**
* Add a flag option (--flag)
*/
flag(name: string): this {
this.options.set(name, true);
return this;
}
/**
* Add a value option (--option=value)
*/
option(name: string, value: string): this {
this.options.set(name, value);
return this;
}
/**
* Add a force flag (--force)
*/
withForce(): this {
return this.flag('force');
}
/**
* Add a no-verify flag (--no-verify)
*/
withNoVerify(): this {
return this.flag('no-verify');
}
/**
* Add a tags flag (--tags)
*/
withTags(): this {
return this.flag('tags');
}
/**
* Add a track flag (--track)
*/
withTrack(): this {
return this.flag('track');
}
/**
* Add a no-track flag (--no-track)
*/
withNoTrack(): this {
return this.flag('no-track');
}
/**
* Add a set-upstream flag (--set-upstream)
*/
withSetUpstream(): this {
return this.flag('set-upstream');
}
/**
* Add an annotated flag (-a)
*/
withAnnotated(): this {
return this.flag('a');
}
/**
* Add a sign flag (-s)
*/
withSign(): this {
return this.flag('s');
}
/**
* Add an include-untracked flag (--include-untracked)
*/
withIncludeUntracked(): this {
return this.flag('include-untracked');
}
/**
* Add a keep-index flag (--keep-index)
*/
withKeepIndex(): this {
return this.flag('keep-index');
}
/**
* Add an all flag (--all)
*/
withAll(): this {
return this.flag('all');
}
/**
* Add a message option (-m "message")
*/
withMessage(message: string): this {
return this.option('m', message);
}
/**
* Build the final command string
*/
toString(): string {
const parts = [...this.command];
// Add options in sorted order for consistency
Array.from(this.options.entries())
.sort(([a], [b]) => a.localeCompare(b))
.forEach(([name, value]) => {
if (name.length === 1) {
// Short option (-m "value")
parts.push(`-${name}`);
if (typeof value === 'string') {
parts.push(this.escapeArg(value));
}
} else {
// Long option
if (value === true) {
// Flag (--force)
parts.push(`--${name}`);
} else if (typeof value === 'string') {
// Value option (--option=value)
parts.push(`--${name}=${this.escapeArg(value)}`);
}
}
});
return parts.join(' ');
}
/**
* Create common Git commands
*/
static init(): GitCommandBuilder {
return new GitCommandBuilder('init');
}
static clone(): GitCommandBuilder {
return new GitCommandBuilder('clone');
}
static add(): GitCommandBuilder {
return new GitCommandBuilder('add');
}
static commit(): GitCommandBuilder {
return new GitCommandBuilder('commit');
}
static push(): GitCommandBuilder {
return new GitCommandBuilder('push');
}
static pull(): GitCommandBuilder {
return new GitCommandBuilder('pull');
}
static branch(): GitCommandBuilder {
return new GitCommandBuilder('branch');
}
static checkout(): GitCommandBuilder {
return new GitCommandBuilder('checkout');
}
static tag(): GitCommandBuilder {
return new GitCommandBuilder('tag');
}
static remote(): GitCommandBuilder {
return new GitCommandBuilder('remote');
}
static stash(): GitCommandBuilder {
return new GitCommandBuilder('stash');
}
static status(): GitCommandBuilder {
return new GitCommandBuilder('status');
}
static fetch(): GitCommandBuilder {
return new GitCommandBuilder('fetch');
}
/**
* Escape command arguments that contain spaces or special characters
*/
private escapeArg(arg: string): string {
if (arg.includes(' ') || arg.includes('"') || arg.includes('\'')) {
// Escape quotes and wrap in quotes
return `"${arg.replace(/"/g, '\\"')}"`;
}
return arg;
}
}
```
--------------------------------------------------------------------------------
/src/operations/base/base-operation.ts:
--------------------------------------------------------------------------------
```typescript
import { CommandExecutor } from '../../utils/command.js';
import { PathValidator } from '../../utils/path.js';
import { logger } from '../../utils/logger.js';
import { repositoryCache } from '../../caching/repository-cache.js';
import { RepoStateType } from '../../caching/repository-cache.js';
import { GitToolContext, GitToolResult } from '../../types.js';
import { GitCommandBuilder } from '../../common/command-builder.js';
import { GitOperationOptions, GitOperationResult, CommandResult } from './operation-result.js';
import { ErrorHandler } from '../../errors/error-handler.js';
import { GitMcpError } from '../../errors/error-types.js';
/**
* Base class for all Git operations providing common functionality
*/
export abstract class BaseGitOperation<TOptions extends GitOperationOptions, TResult = void> {
protected constructor(
protected readonly context: GitToolContext,
protected readonly options: TOptions
) {}
/**
* Execute the Git operation with proper error handling and caching
*/
public async execute(): Promise<GitOperationResult<TResult>> {
try {
// Validate options before proceeding
this.validateOptions();
// Get resolved path
const path = this.getResolvedPath();
// Execute operation with caching if enabled
const result = await this.executeWithCache(path);
// Format the result
return await this.formatResult(result);
} catch (error: unknown) {
return this.handleError(error);
}
}
/**
* Build the Git command for this operation
*/
protected abstract buildCommand(): GitCommandBuilder | Promise<GitCommandBuilder>;
/**
* Parse the command result into operation-specific format
*/
protected abstract parseResult(result: CommandResult): TResult | Promise<TResult>;
/**
* Get cache configuration for this operation
*/
protected abstract getCacheConfig(): {
command: string;
stateType?: RepoStateType;
};
/**
* Validate operation-specific options
*/
protected abstract validateOptions(): void;
/**
* Execute the Git command with caching if enabled
*/
private async executeWithCache(path: string): Promise<CommandResult> {
const { command, stateType } = this.getCacheConfig();
const action = () => this.executeCommand(path);
if (this.options.useCache && path) {
if (stateType) {
// Use state cache
return await repositoryCache.getState(
path,
stateType,
command,
action
);
} else {
// Use command cache
return await repositoryCache.getCommandResult(
path,
command,
action
);
}
}
// Execute without caching
return await action();
}
/**
* Execute the Git command
*/
private async executeCommand(path: string): Promise<CommandResult> {
const builder = await Promise.resolve(this.buildCommand());
const command = builder.toString();
return await CommandExecutor.executeGitCommand(
command,
this.context.operation,
path
);
}
/**
* Format the operation result into standard GitToolResult
*/
private async formatResult(result: CommandResult): Promise<GitOperationResult<TResult>> {
return {
success: true,
data: await Promise.resolve(this.parseResult(result)),
content: [{
type: 'text',
text: CommandExecutor.formatOutput(result)
}]
};
}
/**
* Handle operation errors
*/
private handleError(error: unknown): GitOperationResult<TResult> {
if (error instanceof GitMcpError) {
return {
success: false,
error,
content: [{
type: 'text',
text: error.message
}]
};
}
const wrappedError = ErrorHandler.handleOperationError(
error instanceof Error ? error : new Error('Unknown error'),
{
operation: this.context.operation,
path: this.options.path,
command: this.getCacheConfig().command
}
);
return {
success: false,
error: wrappedError,
content: [{
type: 'text',
text: wrappedError.message
}]
};
}
/**
* Get resolved path with proper validation
*/
protected getResolvedPath(): string {
const path = this.options.path || process.env.GIT_DEFAULT_PATH;
if (!path) {
throw ErrorHandler.handleValidationError(
new Error('Path must be provided when GIT_DEFAULT_PATH is not set'),
{ operation: this.context.operation }
);
}
const { path: repoPath } = PathValidator.validateGitRepo(path);
return repoPath;
}
/**
* Invalidate cache if needed
*/
protected invalidateCache(path: string): void {
if (this.options.invalidateCache) {
const { command, stateType } = this.getCacheConfig();
if (stateType) {
repositoryCache.invalidateState(path, stateType);
}
repositoryCache.invalidateCommand(path, command);
}
}
}
```
--------------------------------------------------------------------------------
/src/operations/tag/tag-operations.ts:
--------------------------------------------------------------------------------
```typescript
import { BaseGitOperation } from '../base/base-operation.js';
import { GitCommandBuilder } from '../../common/command-builder.js';
import { CommandResult } from '../base/operation-result.js';
import { ErrorHandler } from '../../errors/error-handler.js';
import { RepositoryValidator } from '../../utils/repository.js';
import { CommandExecutor } from '../../utils/command.js';
import { RepoStateType } from '../../caching/repository-cache.js';
import {
TagListOptions,
TagCreateOptions,
TagDeleteOptions,
TagListResult,
TagCreateResult,
TagDeleteResult,
TagInfo
} from './tag-types.js';
/**
* Handles Git tag listing operations
*/
export class TagListOperation extends BaseGitOperation<TagListOptions, TagListResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.tag();
// Add format option for parsing
command.option('format', '%(refname:strip=2)|%(objecttype)|%(subject)|%(taggername)|%(taggeremail)|%(taggerdate)|%(objectname)');
if (this.options.showMessage) {
command.flag('n');
}
if (this.options.sort) {
command.option('sort', this.options.sort);
}
if (this.options.contains) {
command.option('contains', this.options.contains);
}
if (this.options.pattern) {
command.arg(this.options.pattern);
}
return command;
}
protected parseResult(result: CommandResult): TagListResult {
const tags: TagInfo[] = [];
// Parse each line of output
result.stdout.split('\n').filter(Boolean).forEach(line => {
const [name, type, message, taggerName, taggerEmail, taggerDate, commit] = line.split('|');
const tag: TagInfo = {
name,
annotated: type === 'tag',
commit,
signed: message?.includes('-----BEGIN PGP SIGNATURE-----') || false
};
if (tag.annotated) {
tag.message = message;
if (taggerName && taggerEmail && taggerDate) {
tag.tagger = {
name: taggerName,
email: taggerEmail.replace(/[<>]/g, ''),
date: taggerDate
};
}
}
tags.push(tag);
});
return {
tags,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'tag',
stateType: RepoStateType.TAG
};
}
protected validateOptions(): void {
// No specific validation needed for listing
}
}
/**
* Handles Git tag creation operations
*/
export class TagCreateOperation extends BaseGitOperation<TagCreateOptions, TagCreateResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.tag();
if (this.options.message) {
command.withAnnotated();
command.withMessage(this.options.message);
}
if (this.options.force) {
command.withForce();
}
if (this.options.sign) {
command.withSign();
}
command.arg(this.options.name);
if (this.options.commit) {
command.arg(this.options.commit);
}
return command;
}
protected parseResult(result: CommandResult): TagCreateResult {
const signed = result.stdout.includes('-----BEGIN PGP SIGNATURE-----');
return {
name: this.options.name,
annotated: Boolean(this.options.message),
signed,
commit: this.options.commit,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'tag_create',
stateType: RepoStateType.TAG
};
}
protected validateOptions(): void {
if (!this.options.name) {
throw ErrorHandler.handleValidationError(
new Error('Tag name is required'),
{ operation: this.context.operation }
);
}
}
}
/**
* Handles Git tag deletion operations
*/
export class TagDeleteOperation extends BaseGitOperation<TagDeleteOptions, TagDeleteResult> {
protected async buildCommand(): Promise<GitCommandBuilder> {
const command = GitCommandBuilder.tag();
command.flag('d');
if (this.options.force) {
command.withForce();
}
command.arg(this.options.name);
if (this.options.remote) {
// Get remote name from configuration
const remotes = await RepositoryValidator.getRemotes(
this.getResolvedPath(),
this.context.operation
);
// Push deletion to all remotes
for (const remote of remotes) {
await CommandExecutor.executeGitCommand(
`push ${remote} :refs/tags/${this.options.name}`,
this.context.operation,
this.getResolvedPath()
);
}
}
return command;
}
protected parseResult(result: CommandResult): TagDeleteResult {
return {
name: this.options.name,
forced: this.options.force || false,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'tag_delete',
stateType: RepoStateType.TAG
};
}
protected async validateOptions(): Promise<void> {
if (!this.options.name) {
throw ErrorHandler.handleValidationError(
new Error('Tag name is required'),
{ operation: this.context.operation }
);
}
// Ensure tag exists
await RepositoryValidator.validateTagExists(
this.getResolvedPath(),
this.options.name,
this.context.operation
);
}
}
```
--------------------------------------------------------------------------------
/src/caching/repository-cache.ts:
--------------------------------------------------------------------------------
```typescript
import { RepositoryStateCache, CommandResultCache } from './cache.js';
import { logger } from '../utils/logger.js';
import { PerformanceMonitor } from '../monitoring/performance.js';
/**
* Repository state types
*/
export enum RepoStateType {
BRANCH = 'branch',
STATUS = 'status',
REMOTE = 'remote',
TAG = 'tag',
STASH = 'stash'
}
/**
* Repository state manager with caching
*/
export class RepositoryCacheManager {
private static instance: RepositoryCacheManager;
private stateCache: RepositoryStateCache;
private commandCache: CommandResultCache;
private performanceMonitor: PerformanceMonitor;
private constructor() {
this.stateCache = new RepositoryStateCache();
this.commandCache = new CommandResultCache();
this.performanceMonitor = PerformanceMonitor.getInstance();
}
/**
* Get singleton instance
*/
static getInstance(): RepositoryCacheManager {
if (!RepositoryCacheManager.instance) {
RepositoryCacheManager.instance = new RepositoryCacheManager();
}
return RepositoryCacheManager.instance;
}
/**
* Get repository state from cache or execute command
*/
async getState(
repoPath: string,
stateType: RepoStateType,
command: string,
executor: () => Promise<any>
): Promise<any> {
const cacheKey = this.getStateKey(repoPath, stateType);
const cachedState = this.stateCache.get(cacheKey);
if (cachedState !== undefined) {
logger.debug(
'cache',
`Cache hit for repository state: ${stateType}`,
repoPath,
{ command }
);
return cachedState;
}
// Start timing the operation
const startTime = performance.now();
try {
const result = await executor();
const duration = performance.now() - startTime;
// Record performance metrics
this.performanceMonitor.recordCommandExecution(command, duration, {
repoPath,
stateType,
cached: false
});
// Cache the result
this.stateCache.set(cacheKey, result);
return result;
} catch (error) {
const duration = performance.now() - startTime;
this.performanceMonitor.recordCommandExecution(command, duration, {
repoPath,
stateType,
cached: false,
error: true
});
throw error;
}
}
/**
* Get command result from cache or execute command
*/
async getCommandResult(
repoPath: string,
command: string,
executor: () => Promise<any>
): Promise<any> {
const cacheKey = CommandResultCache.generateKey(command, repoPath);
const cachedResult = this.commandCache.get(cacheKey);
if (cachedResult !== undefined) {
logger.debug(
'cache',
`Cache hit for command result`,
repoPath,
{ command }
);
return cachedResult;
}
// Start timing the operation
const startTime = performance.now();
try {
const result = await executor();
const duration = performance.now() - startTime;
// Record performance metrics
this.performanceMonitor.recordCommandExecution(command, duration, {
repoPath,
cached: false
});
// Cache the result
this.commandCache.set(cacheKey, result);
return result;
} catch (error) {
const duration = performance.now() - startTime;
this.performanceMonitor.recordCommandExecution(command, duration, {
repoPath,
cached: false,
error: true
});
throw error;
}
}
/**
* Invalidate repository state cache
*/
invalidateState(repoPath: string, stateType?: RepoStateType): void {
if (stateType) {
const cacheKey = this.getStateKey(repoPath, stateType);
this.stateCache.delete(cacheKey);
logger.debug(
'cache',
`Invalidated repository state cache`,
repoPath,
{ stateType }
);
} else {
// Invalidate all state types for this repository
Object.values(RepoStateType).forEach(type => {
const cacheKey = this.getStateKey(repoPath, type);
this.stateCache.delete(cacheKey);
});
logger.debug(
'cache',
`Invalidated all repository state cache`,
repoPath
);
}
}
/**
* Invalidate command result cache
*/
invalidateCommand(repoPath: string, command?: string): void {
if (command) {
const cacheKey = CommandResultCache.generateKey(command, repoPath);
this.commandCache.delete(cacheKey);
logger.debug(
'cache',
`Invalidated command result cache`,
repoPath,
{ command }
);
} else {
// Clear all command results for this repository
// Note: This is a bit inefficient as it clears all commands for all repos
// A better solution would be to store repo-specific commands separately
this.commandCache.clear();
logger.debug(
'cache',
`Invalidated all command result cache`,
repoPath
);
}
}
/**
* Get cache statistics
*/
getStats(): Record<string, any> {
return {
state: this.stateCache.getStats(),
command: this.commandCache.getStats()
};
}
/**
* Generate cache key for repository state
*/
private getStateKey(repoPath: string, stateType: RepoStateType): string {
return `${repoPath}:${stateType}`;
}
}
// Export singleton instance
export const repositoryCache = RepositoryCacheManager.getInstance();
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
import { ExecOptions } from 'child_process';
export enum ErrorCode {
InvalidParams = 'InvalidParams',
InternalError = 'InternalError',
InvalidState = 'InvalidState'
}
export interface GitOptions {
/**
* Absolute path to the working directory
* Example: /Users/username/projects/my-repo
*/
cwd?: string;
execOptions?: ExecOptions;
}
export interface GitToolContent {
type: string;
text: string;
}
export interface GitToolResult {
content: GitToolContent[];
_meta?: Record<string, unknown>;
}
export interface GitToolContext {
operation: string;
path?: string;
options?: GitOptions;
}
// Base interface for operations that require a path
export interface BasePathOptions {
/**
* MUST be an absolute path to the repository
* Example: /Users/username/projects/my-repo
* If not provided, will use GIT_DEFAULT_PATH from environment
*/
path?: string;
}
// Tool-specific interfaces
export interface InitOptions extends GitOptions, BasePathOptions {}
export interface CloneOptions extends GitOptions, BasePathOptions {
/**
* URL of the repository to clone
*/
url: string;
}
export interface AddOptions extends GitOptions, BasePathOptions {
/**
* Array of absolute paths to files to stage
* Example: /Users/username/projects/my-repo/src/file.js
*/
files: string[];
}
export interface CommitOptions extends GitOptions, BasePathOptions {
message: string;
}
export interface PushPullOptions extends GitOptions, BasePathOptions {
remote?: string;
branch: string;
force?: boolean; // Allow force push/pull
noVerify?: boolean; // Skip pre-push/pre-pull hooks
tags?: boolean; // Include tags
}
export interface BranchOptions extends GitOptions, BasePathOptions {
name: string;
force?: boolean; // Allow force operations
track?: boolean; // Set up tracking mode
setUpstream?: boolean; // Set upstream for push/pull
}
export interface CheckoutOptions extends GitOptions, BasePathOptions {
target: string;
}
export interface TagOptions extends GitOptions, BasePathOptions {
name: string;
message?: string;
force?: boolean; // Allow force operations
annotated?: boolean; // Create an annotated tag
sign?: boolean; // Create a signed tag
}
export interface RemoteOptions extends GitOptions, BasePathOptions {
name: string;
url?: string;
force?: boolean; // Allow force operations
mirror?: boolean; // Mirror all refs
tags?: boolean; // Include tags
}
export interface StashOptions extends GitOptions, BasePathOptions {
message?: string;
index?: number;
includeUntracked?: boolean; // Include untracked files
keepIndex?: boolean; // Keep staged changes
all?: boolean; // Include ignored files
}
// New bulk action interfaces
export interface BulkActionStage {
type: 'stage';
files?: string[]; // If not provided, stages all files
}
export interface BulkActionCommit {
type: 'commit';
message: string;
}
export interface BulkActionPush {
type: 'push';
remote?: string;
branch: string;
}
export type BulkAction = BulkActionStage | BulkActionCommit | BulkActionPush;
export interface BulkActionOptions extends GitOptions, BasePathOptions {
actions: BulkAction[];
}
// Type guard functions
export function isAbsolutePath(path: string): boolean {
return path.startsWith('/');
}
export function validatePath(path?: string): boolean {
return !path || isAbsolutePath(path);
}
export function isInitOptions(obj: any): obj is InitOptions {
return obj && validatePath(obj.path);
}
export function isCloneOptions(obj: any): obj is CloneOptions {
return obj &&
typeof obj.url === 'string' &&
validatePath(obj.path);
}
export function isAddOptions(obj: any): obj is AddOptions {
return obj &&
validatePath(obj.path) &&
Array.isArray(obj.files) &&
obj.files.every((f: any) => typeof f === 'string' && isAbsolutePath(f));
}
export function isCommitOptions(obj: any): obj is CommitOptions {
return obj &&
validatePath(obj.path) &&
typeof obj.message === 'string';
}
export function isPushPullOptions(obj: any): obj is PushPullOptions {
return obj &&
validatePath(obj.path) &&
typeof obj.branch === 'string';
}
export function isBranchOptions(obj: any): obj is BranchOptions {
return obj &&
validatePath(obj.path) &&
typeof obj.name === 'string';
}
export function isCheckoutOptions(obj: any): obj is CheckoutOptions {
return obj &&
validatePath(obj.path) &&
typeof obj.target === 'string';
}
export function isTagOptions(obj: any): obj is TagOptions {
return obj &&
validatePath(obj.path) &&
typeof obj.name === 'string';
}
export function isRemoteOptions(obj: any): obj is RemoteOptions {
return obj &&
validatePath(obj.path) &&
typeof obj.name === 'string';
}
export function isStashOptions(obj: any): obj is StashOptions {
return obj && validatePath(obj.path);
}
export function isPathOnly(obj: any): obj is BasePathOptions {
return obj && validatePath(obj.path);
}
export function isBulkActionOptions(obj: any): obj is BulkActionOptions {
if (!obj || !validatePath(obj.path) || !Array.isArray(obj.actions)) {
return false;
}
return obj.actions.every((action: any) => {
if (!action || typeof action.type !== 'string') {
return false;
}
switch (action.type) {
case 'stage':
return !action.files || (Array.isArray(action.files) &&
action.files.every((f: any) => typeof f === 'string' && isAbsolutePath(f)));
case 'commit':
return typeof action.message === 'string';
case 'push':
return typeof action.branch === 'string';
default:
return false;
}
});
}
```
--------------------------------------------------------------------------------
/src/utils/paths.ts:
--------------------------------------------------------------------------------
```typescript
import { resolve, isAbsolute, normalize, relative, join, dirname } from 'path';
import { existsSync, statSync, mkdirSync } from 'fs';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { logger } from './logger.js';
export interface PathInfo {
original: string;
absolute: string;
relative: string;
exists: boolean;
isDirectory?: boolean;
isFile?: boolean;
isGitRepo?: boolean;
parent: string;
}
export class PathResolver {
private static readonly CWD = process.cwd();
private static createDirectory(path: string, operation: string): void {
try {
mkdirSync(path, { recursive: true });
logger.info(operation, `Created directory: ${path}`);
} catch (error) {
logger.error(operation, `Failed to create directory: ${path}`, path, error as Error);
throw new McpError(
ErrorCode.InternalError,
`Failed to create directory: ${(error as Error).message}`
);
}
}
private static getStats(path: string): { exists: boolean; isDirectory?: boolean; isFile?: boolean } {
if (!existsSync(path)) {
return { exists: false };
}
try {
const stats = statSync(path);
return {
exists: true,
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
};
} catch {
return { exists: true };
}
}
private static validateAbsolutePath(path: string, operation: string): void {
if (!isAbsolute(path)) {
const error = new McpError(
ErrorCode.InvalidParams,
`Path must be absolute. Received: ${path}\nExample: /Users/username/projects/my-repo`
);
logger.error(operation, 'Invalid path format', path, error);
throw error;
}
}
static getPathInfo(path: string, operation: string): PathInfo {
logger.debug(operation, 'Resolving path info', path);
// Validate absolute path
this.validateAbsolutePath(path, operation);
// Normalize the path
const absolutePath = normalize(path);
const relativePath = relative(this.CWD, absolutePath);
const parentPath = dirname(absolutePath);
// Get path stats
const stats = this.getStats(absolutePath);
const isGitRepo = stats.isDirectory ? existsSync(join(absolutePath, '.git')) : false;
const pathInfo: PathInfo = {
original: path,
absolute: absolutePath,
relative: relativePath,
exists: stats.exists,
isDirectory: stats.isDirectory,
isFile: stats.isFile,
isGitRepo,
parent: parentPath,
};
logger.debug(operation, 'Path info resolved', path, pathInfo);
return pathInfo;
}
static validatePath(path: string, operation: string, options: {
mustExist?: boolean;
mustBeDirectory?: boolean;
mustBeFile?: boolean;
mustBeGitRepo?: boolean;
createIfMissing?: boolean;
} = {}): PathInfo {
const {
mustExist = false,
mustBeDirectory = false,
mustBeFile = false,
mustBeGitRepo = false,
createIfMissing = false,
} = options;
logger.debug(operation, 'Validating path with options', path, options);
// Get path info (includes absolute path validation)
const pathInfo = this.getPathInfo(path, operation);
// Create directory if needed
if (!pathInfo.exists && (createIfMissing || mustBeDirectory)) {
this.createDirectory(pathInfo.absolute, operation);
return this.getPathInfo(path, operation);
}
// Handle existence requirements
if (mustExist && !pathInfo.exists) {
const error = new McpError(
ErrorCode.InvalidParams,
`Path does not exist: ${pathInfo.absolute}`
);
logger.error(operation, 'Path validation failed', path, error);
throw error;
}
// Validate directory requirement
if (mustBeDirectory && !pathInfo.isDirectory) {
const error = new McpError(
ErrorCode.InvalidParams,
`Path is not a directory: ${pathInfo.absolute}`
);
logger.error(operation, 'Path validation failed', path, error);
throw error;
}
// Validate file requirement
if (mustBeFile && !pathInfo.isFile) {
const error = new McpError(
ErrorCode.InvalidParams,
`Path is not a file: ${pathInfo.absolute}`
);
logger.error(operation, 'Path validation failed', path, error);
throw error;
}
// Validate git repo requirement
if (mustBeGitRepo && !pathInfo.isGitRepo) {
const error = new McpError(
ErrorCode.InvalidParams,
`Path is not a git repository: ${pathInfo.absolute}`
);
logger.error(operation, 'Path validation failed', path, error);
throw error;
}
logger.debug(operation, 'Path validation successful', path, pathInfo);
return pathInfo;
}
static validateFilePaths(paths: string[], operation: string): PathInfo[] {
logger.debug(operation, 'Validating multiple file paths', undefined, { paths });
return paths.map(path => {
// Validate absolute path
this.validateAbsolutePath(path, operation);
const pathInfo = this.validatePath(path, operation, {
mustExist: true,
mustBeFile: true,
});
return pathInfo;
});
}
static validateGitRepo(path: string, operation: string): PathInfo {
// Validate absolute path
this.validateAbsolutePath(path, operation);
return this.validatePath(path, operation, {
mustExist: true,
mustBeDirectory: true,
mustBeGitRepo: true,
});
}
static ensureDirectory(path: string, operation: string): PathInfo {
// Validate absolute path
this.validateAbsolutePath(path, operation);
return this.validatePath(path, operation, {
createIfMissing: true,
mustBeDirectory: true,
});
}
}
```
--------------------------------------------------------------------------------
/src/utils/command.ts:
--------------------------------------------------------------------------------
```typescript
import { ExecException, exec, ExecOptions } from 'child_process';
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import { logger } from './logger.js';
import { PathResolver } from './paths.js';
import { ErrorHandler } from '../errors/error-handler.js';
import { ErrorCategory, ErrorSeverity, GitMcpError } from '../errors/error-types.js';
export interface CommandResult {
stdout: string;
stderr: string;
command: string;
workingDir?: string;
}
/**
* Formats a command error message with detailed context
*/
function formatCommandError(error: ExecException, result: Partial<CommandResult>): string {
let message = `Command failed with exit code ${error.code}`;
if (result.command) {
message += `\nCommand: ${result.command}`;
}
if (result.workingDir) {
message += `\nWorking Directory: ${result.workingDir}`;
}
if (result.stdout) {
message += `\nOutput: ${result.stdout}`;
}
if (result.stderr) {
message += `\nError: ${result.stderr}`;
}
return message;
}
/**
* Creates a command error with appropriate category and severity
*/
function createCommandError(
error: ExecException,
result: Partial<CommandResult>,
operation: string
): GitMcpError {
const message = formatCommandError(error, result);
const context = {
operation,
path: result.workingDir,
command: result.command,
details: {
exitCode: error.code,
stdout: result.stdout,
stderr: result.stderr
}
};
// Determine error category and severity based on error code and context
const errorCode = error.code?.toString() || '';
// System errors
if (errorCode === 'ENOENT') {
return ErrorHandler.handleSystemError(error, context);
}
// Security errors
if (errorCode === 'EACCES') {
return ErrorHandler.handleSecurityError(error, context);
}
// Validation errors
if (errorCode === 'ENOTDIR' || errorCode === 'ENOTEMPTY') {
return ErrorHandler.handleValidationError(error, context);
}
// Git-specific error codes
const numericCode = typeof error.code === 'number' ? error.code :
typeof error.code === 'string' ? parseInt(error.code, 10) :
null;
if (numericCode !== null) {
switch (numericCode) {
case 128: // Repository not found or invalid
return ErrorHandler.handleRepositoryError(error, context);
case 129: // Invalid command or argument
return ErrorHandler.handleValidationError(error, context);
case 130: // User interrupt
return ErrorHandler.handleOperationError(error, context);
default:
return ErrorHandler.handleOperationError(error, context);
}
}
// Default to operation error for unknown cases
return ErrorHandler.handleOperationError(error, context);
}
export class CommandExecutor {
static async execute(
command: string,
operation: string,
workingDir?: string,
options: ExecOptions = {}
): Promise<CommandResult> {
// Validate and resolve working directory if provided
if (workingDir) {
const pathInfo = PathResolver.validatePath(workingDir, operation, {
mustExist: true,
mustBeDirectory: true,
});
workingDir = pathInfo.absolute;
}
// Log command execution
logger.logCommand(operation, command, workingDir);
// Prepare execution options
const execOptions: ExecOptions = {
...options,
cwd: workingDir,
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
};
return new Promise((resolve, reject) => {
exec(command, execOptions, (error, stdout, stderr) => {
const result: CommandResult = {
command,
workingDir,
stdout: stdout.trim(),
stderr: stderr.trim(),
};
// Log command result
if (error) {
reject(createCommandError(error, result, operation));
return;
}
logger.logCommandResult(operation, result.stdout, workingDir, {
stderr: result.stderr,
});
resolve(result);
});
});
}
static formatOutput(result: CommandResult): string {
let output = '';
if (result.stdout) {
output += result.stdout;
}
if (result.stderr) {
if (output) output += '\n';
output += result.stderr;
}
return output.trim();
}
static async executeGitCommand(
command: string,
operation: string,
workingDir?: string,
options: ExecOptions = {}
): Promise<CommandResult> {
// Add git environment variables
const gitOptions: ExecOptions = {
...options,
env: {
...process.env,
...options.env,
GIT_TERMINAL_PROMPT: '0', // Disable git terminal prompts
GIT_ASKPASS: 'echo', // Prevent password prompts
},
};
try {
return await this.execute(`git ${command}`, operation, workingDir, gitOptions);
} catch (error) {
if (error instanceof GitMcpError) {
// Add git-specific context to error
logger.error(operation, 'Git command failed', workingDir, error, {
command: `git ${command}`,
gitConfig: await this.execute('git config --list', operation, workingDir)
.then(result => result.stdout)
.catch(() => 'Unable to get git config'),
});
}
throw error;
}
}
static async validateGitInstallation(operation: string): Promise<void> {
try {
const result = await this.execute('git --version', operation);
logger.info(operation, 'Git installation validated', undefined, {
version: result.stdout,
});
} catch (error) {
const mcpError = new McpError(
ErrorCode.InternalError,
'Git is not installed or not accessible'
);
logger.error(operation, 'Git installation validation failed', undefined, mcpError);
throw mcpError;
}
}
}
```
--------------------------------------------------------------------------------
/src/caching/cache.ts:
--------------------------------------------------------------------------------
```typescript
import { logger } from '../utils/logger.js';
import { PerformanceMonitor } from '../monitoring/performance.js';
/**
* Cache entry with metadata
*/
interface CacheEntry<T> {
value: T;
timestamp: number;
ttl: number;
hits: number;
lastAccess: number;
}
/**
* Cache configuration
*/
interface CacheConfig {
defaultTTL: number; // Default time-to-live in milliseconds
maxSize: number; // Maximum number of entries
cleanupInterval: number; // Cleanup interval in milliseconds
}
/**
* Default cache configuration
*/
const DEFAULT_CONFIG: CacheConfig = {
defaultTTL: 5 * 60 * 1000, // 5 minutes
maxSize: 1000, // 1000 entries
cleanupInterval: 60 * 1000 // 1 minute
};
/**
* Generic cache implementation with performance monitoring
*/
export class Cache<T> {
private entries: Map<string, CacheEntry<T>> = new Map();
private config: CacheConfig;
private performanceMonitor: PerformanceMonitor;
private readonly cacheType: string;
constructor(cacheType: string, config: Partial<CacheConfig> = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.cacheType = cacheType;
this.performanceMonitor = PerformanceMonitor.getInstance();
this.startCleanup();
}
/**
* Set a cache entry
*/
set(key: string, value: T, ttl: number = this.config.defaultTTL): void {
// Check cache size limit
if (this.entries.size >= this.config.maxSize) {
this.evictOldest();
}
this.entries.set(key, {
value,
timestamp: Date.now(),
ttl,
hits: 0,
lastAccess: Date.now()
});
logger.debug(
'cache',
`Set cache entry: ${key}`,
undefined,
{ cacheType: this.cacheType }
);
}
/**
* Get a cache entry
*/
get(key: string): T | undefined {
const entry = this.entries.get(key);
if (!entry) {
this.performanceMonitor.recordCacheAccess(false, this.cacheType);
return undefined;
}
// Check if entry is expired
if (this.isExpired(entry)) {
this.entries.delete(key);
this.performanceMonitor.recordCacheAccess(false, this.cacheType);
return undefined;
}
// Update entry metadata
entry.hits++;
entry.lastAccess = Date.now();
this.performanceMonitor.recordCacheAccess(true, this.cacheType);
logger.debug(
'cache',
`Cache hit: ${key}`,
undefined,
{ cacheType: this.cacheType, hits: entry.hits }
);
return entry.value;
}
/**
* Delete a cache entry
*/
delete(key: string): void {
this.entries.delete(key);
logger.debug(
'cache',
`Deleted cache entry: ${key}`,
undefined,
{ cacheType: this.cacheType }
);
}
/**
* Clear all cache entries
*/
clear(): void {
this.entries.clear();
logger.info(
'cache',
'Cleared cache',
undefined,
{ cacheType: this.cacheType }
);
}
/**
* Get cache statistics
*/
getStats(): Record<string, any> {
const now = Date.now();
let totalHits = 0;
let totalSize = 0;
let oldestTimestamp = now;
let newestTimestamp = 0;
this.entries.forEach(entry => {
totalHits += entry.hits;
totalSize++;
oldestTimestamp = Math.min(oldestTimestamp, entry.timestamp);
newestTimestamp = Math.max(newestTimestamp, entry.timestamp);
});
return {
size: totalSize,
maxSize: this.config.maxSize,
totalHits,
oldestEntry: oldestTimestamp === now ? null : oldestTimestamp,
newestEntry: newestTimestamp === 0 ? null : newestTimestamp,
hitRate: this.performanceMonitor.getCacheHitRate(this.cacheType)
};
}
/**
* Check if a cache entry exists and is valid
*/
has(key: string): boolean {
const entry = this.entries.get(key);
if (!entry) return false;
if (this.isExpired(entry)) {
this.entries.delete(key);
return false;
}
return true;
}
/**
* Update cache configuration
*/
updateConfig(config: Partial<CacheConfig>): void {
this.config = {
...this.config,
...config
};
logger.info(
'cache',
'Updated cache configuration',
undefined,
{ cacheType: this.cacheType, config: this.config }
);
}
/**
* Get current configuration
*/
getConfig(): CacheConfig {
return { ...this.config };
}
/**
* Check if a cache entry is expired
*/
private isExpired(entry: CacheEntry<T>): boolean {
return Date.now() - entry.timestamp > entry.ttl;
}
/**
* Evict the least recently used entry
*/
private evictOldest(): void {
let oldestKey: string | undefined;
let oldestAccess = Date.now();
this.entries.forEach((entry, key) => {
if (entry.lastAccess < oldestAccess) {
oldestAccess = entry.lastAccess;
oldestKey = key;
}
});
if (oldestKey) {
this.entries.delete(oldestKey);
logger.debug(
'cache',
`Evicted oldest entry: ${oldestKey}`,
undefined,
{ cacheType: this.cacheType }
);
}
}
/**
* Start periodic cache cleanup
*/
private startCleanup(): void {
setInterval(() => {
const now = Date.now();
let expiredCount = 0;
this.entries.forEach((entry, key) => {
if (now - entry.timestamp > entry.ttl) {
this.entries.delete(key);
expiredCount++;
}
});
if (expiredCount > 0) {
logger.debug(
'cache',
`Cleaned up ${expiredCount} expired entries`,
undefined,
{ cacheType: this.cacheType }
);
}
}, this.config.cleanupInterval);
}
}
/**
* Repository state cache
*/
export class RepositoryStateCache extends Cache<any> {
constructor() {
super('repository_state', {
defaultTTL: 30 * 1000, // 30 seconds
maxSize: 100 // 100 entries
});
}
}
/**
* Command result cache
*/
export class CommandResultCache extends Cache<any> {
constructor() {
super('command_result', {
defaultTTL: 5 * 60 * 1000, // 5 minutes
maxSize: 500 // 500 entries
});
}
/**
* Generate cache key for a command
*/
static generateKey(command: string, workingDir?: string): string {
return `${workingDir || ''}:${command}`;
}
}
```
--------------------------------------------------------------------------------
/src/errors/error-handler.ts:
--------------------------------------------------------------------------------
```typescript
import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import {
ErrorCategory,
ErrorSeverity,
ErrorCategoryType,
ErrorSeverityType,
GitMcpError,
ErrorContext
} from './error-types.js';
import { logger } from '../utils/logger.js';
/**
* Maps error categories to appropriate MCP error codes
*/
function getMcpErrorCode(category: ErrorCategoryType, severity: ErrorSeverityType): ErrorCode {
switch (category) {
case ErrorCategory.VALIDATION:
return ErrorCode.InvalidParams;
case ErrorCategory.SYSTEM:
case ErrorCategory.OPERATION:
case ErrorCategory.NETWORK:
case ErrorCategory.SECURITY:
return ErrorCode.InternalError;
case ErrorCategory.REPOSITORY:
// For repository state errors, use InvalidParams if it's a configuration issue,
// otherwise use InternalError
return severity === ErrorSeverity.MEDIUM ? ErrorCode.InvalidParams : ErrorCode.InternalError;
case ErrorCategory.CONFIGURATION:
return ErrorCode.InvalidParams;
default:
return ErrorCode.InternalError;
}
}
/**
* Handles and logs errors with appropriate context and recovery steps
*/
export class ErrorHandler {
/**
* Creates a GitMcpError with appropriate context and logs it
*/
static handleError(
error: Error | GitMcpError,
category: ErrorCategoryType,
severity: ErrorSeverityType,
context: Partial<ErrorContext>
): GitMcpError {
// If it's already a GitMcpError, just log and return it
if (error instanceof GitMcpError) {
this.logError(error);
return error;
}
// Create new GitMcpError with context
const errorContext: Partial<ErrorContext> = {
...context,
stackTrace: error.stack,
timestamp: Date.now()
};
const gitError = new GitMcpError(
getMcpErrorCode(category, severity),
error.message,
severity,
category,
errorContext
);
this.logError(gitError);
return gitError;
}
/**
* Logs error with full context and recovery steps
*/
private static logError(error: GitMcpError): void {
const errorInfo = {
name: error.name,
message: error.message,
severity: error.severity,
category: error.category,
context: error.context,
recoverySteps: error.getRecoverySteps()
};
// Log based on severity
switch (error.severity) {
case ErrorSeverity.CRITICAL:
logger.error(
error.context.operation,
`CRITICAL: ${error.message}`,
error.context.path,
error,
errorInfo
);
break;
case ErrorSeverity.HIGH:
logger.error(
error.context.operation,
`HIGH: ${error.message}`,
error.context.path,
error,
errorInfo
);
break;
case ErrorSeverity.MEDIUM:
logger.warn(
error.context.operation,
`MEDIUM: ${error.message}`,
error.context.path,
error,
errorInfo
);
break;
case ErrorSeverity.LOW:
logger.warn(
error.context.operation,
`LOW: ${error.message}`,
error.context.path,
error,
errorInfo
);
break;
}
}
/**
* Creates and handles a system error
*/
static handleSystemError(error: Error, context: Partial<ErrorContext>): GitMcpError {
return this.handleError(error, ErrorCategory.SYSTEM, ErrorSeverity.CRITICAL, context);
}
/**
* Creates and handles a validation error
*/
static handleValidationError(error: Error, context: Partial<ErrorContext>): GitMcpError {
return this.handleError(error, ErrorCategory.VALIDATION, ErrorSeverity.HIGH, context);
}
/**
* Creates and handles an operation error
*/
static handleOperationError(error: Error, context: Partial<ErrorContext>): GitMcpError {
return this.handleError(error, ErrorCategory.OPERATION, ErrorSeverity.HIGH, context);
}
/**
* Creates and handles a repository error
*/
static handleRepositoryError(error: Error, context: Partial<ErrorContext>): GitMcpError {
return this.handleError(error, ErrorCategory.REPOSITORY, ErrorSeverity.HIGH, context);
}
/**
* Creates and handles a network error
*/
static handleNetworkError(error: Error, context: Partial<ErrorContext>): GitMcpError {
return this.handleError(error, ErrorCategory.NETWORK, ErrorSeverity.HIGH, context);
}
/**
* Creates and handles a configuration error
*/
static handleConfigError(error: Error, context: Partial<ErrorContext>): GitMcpError {
return this.handleError(error, ErrorCategory.CONFIGURATION, ErrorSeverity.MEDIUM, context);
}
/**
* Creates and handles a security error
*/
static handleSecurityError(error: Error, context: Partial<ErrorContext>): GitMcpError {
return this.handleError(error, ErrorCategory.SECURITY, ErrorSeverity.CRITICAL, context);
}
/**
* Determines if an error is retryable based on its category and severity
*/
static isRetryable(error: GitMcpError): boolean {
// Never retry validation or security errors
if (
error.category === ErrorCategory.VALIDATION ||
error.category === ErrorCategory.SECURITY ||
error.severity === ErrorSeverity.CRITICAL
) {
return false;
}
// Network errors are usually retryable
if (error.category === ErrorCategory.NETWORK) {
return true;
}
// Repository and operation errors are retryable for non-critical severities
if (
(error.category === ErrorCategory.REPOSITORY ||
error.category === ErrorCategory.OPERATION) &&
[ErrorSeverity.HIGH, ErrorSeverity.MEDIUM, ErrorSeverity.LOW].includes(error.severity as any)
) {
return true;
}
return false;
}
/**
* Gets suggested retry delay in milliseconds based on error type
*/
static getRetryDelay(error: GitMcpError): number {
if (!this.isRetryable(error)) {
return 0;
}
switch (error.category) {
case ErrorCategory.NETWORK:
return 1000; // 1 second for network issues
case ErrorCategory.REPOSITORY:
return 500; // 500ms for repository issues
case ErrorCategory.OPERATION:
return 200; // 200ms for operation issues
default:
return 1000; // Default 1 second
}
}
}
```
--------------------------------------------------------------------------------
/src/operations/branch/branch-operations.ts:
--------------------------------------------------------------------------------
```typescript
import { BaseGitOperation } from '../base/base-operation.js';
import { GitCommandBuilder } from '../../common/command-builder.js';
import { CommandResult } from '../base/operation-result.js';
import { ErrorHandler } from '../../errors/error-handler.js';
import { RepositoryValidator } from '../../utils/repository.js';
import { RepoStateType } from '../../caching/repository-cache.js';
import {
BranchListOptions,
BranchCreateOptions,
BranchDeleteOptions,
CheckoutOptions,
BranchListResult,
BranchCreateResult,
BranchDeleteResult,
CheckoutResult,
BranchInfo
} from './branch-types.js';
/**
* Handles Git branch listing operations
*/
export class BranchListOperation extends BaseGitOperation<BranchListOptions, BranchListResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.branch();
// Add format option for parsing
command.option('format', '%(refname:short)|%(upstream:short)|%(objectname:short)|%(subject)');
if (this.options.remotes) {
command.flag('remotes');
}
if (this.options.all) {
command.flag('all');
}
if (this.options.contains) {
command.option('contains', this.options.contains);
}
if (this.options.merged) {
command.option('merged', this.options.merged);
}
if (this.options.noMerged) {
command.option('no-merged', this.options.noMerged);
}
return command;
}
protected parseResult(result: CommandResult): BranchListResult {
const branches: BranchInfo[] = [];
let current = '';
// Parse each line of output
result.stdout.split('\n').filter(Boolean).forEach(line => {
const [name, tracking, commit, message] = line.split('|');
const isCurrent = name.startsWith('* ');
const cleanName = name.replace('* ', '');
const branch: BranchInfo = {
name: cleanName,
current: isCurrent,
tracking: tracking || undefined,
remote: cleanName.includes('origin/'),
commit: commit || undefined,
message: message || undefined
};
if (isCurrent) {
current = cleanName;
}
branches.push(branch);
});
return {
current,
branches,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'branch',
stateType: RepoStateType.BRANCH
};
}
protected validateOptions(): void {
// No specific validation needed for listing
}
}
/**
* Handles Git branch creation operations
*/
export class BranchCreateOperation extends BaseGitOperation<BranchCreateOptions, BranchCreateResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.branch()
.arg(this.options.name);
if (this.options.startPoint) {
command.arg(this.options.startPoint);
}
if (this.options.force) {
command.withForce();
}
if (this.options.track) {
command.withTrack();
} else {
command.withNoTrack();
}
if (this.options.setUpstream) {
command.withSetUpstream();
}
return command;
}
protected parseResult(result: CommandResult): BranchCreateResult {
return {
name: this.options.name,
startPoint: this.options.startPoint,
tracking: result.stdout.includes('set up to track') ?
result.stdout.match(/track\s+([^\s]+)/)?.[1] : undefined,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'branch_create',
stateType: RepoStateType.BRANCH
};
}
protected validateOptions(): void {
if (!this.options.name) {
throw ErrorHandler.handleValidationError(
new Error('Branch name is required'),
{ operation: this.context.operation }
);
}
}
}
/**
* Handles Git branch deletion operations
*/
export class BranchDeleteOperation extends BaseGitOperation<BranchDeleteOptions, BranchDeleteResult> {
protected async buildCommand(): Promise<GitCommandBuilder> {
const command = GitCommandBuilder.branch();
// Use -D for force delete, -d for safe delete
command.flag(this.options.force ? 'D' : 'd')
.arg(this.options.name);
if (this.options.remote) {
// Get remote name from branch if it's a remote branch
const remoteName = this.options.name.split('/')[0];
if (remoteName) {
await RepositoryValidator.validateRemoteConfig(
this.getResolvedPath(),
remoteName,
this.context.operation
);
}
command.flag('r');
}
return command;
}
protected parseResult(result: CommandResult): BranchDeleteResult {
return {
name: this.options.name,
forced: this.options.force || false,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'branch_delete',
stateType: RepoStateType.BRANCH
};
}
protected async validateOptions(): Promise<void> {
if (!this.options.name) {
throw ErrorHandler.handleValidationError(
new Error('Branch name is required'),
{ operation: this.context.operation }
);
}
// Ensure branch exists
await RepositoryValidator.validateBranchExists(
this.getResolvedPath(),
this.options.name,
this.context.operation
);
// Cannot delete current branch
const currentBranch = await RepositoryValidator.getCurrentBranch(
this.getResolvedPath(),
this.context.operation
);
if (currentBranch === this.options.name) {
throw ErrorHandler.handleValidationError(
new Error(`Cannot delete the currently checked out branch: ${this.options.name}`),
{ operation: this.context.operation }
);
}
}
}
/**
* Handles Git checkout operations
*/
export class CheckoutOperation extends BaseGitOperation<CheckoutOptions, CheckoutResult> {
protected async buildCommand(): Promise<GitCommandBuilder> {
const command = GitCommandBuilder.checkout();
if (this.options.newBranch) {
command.flag('b').arg(this.options.newBranch);
if (this.options.track) {
command.withTrack();
}
}
command.arg(this.options.target);
if (this.options.force) {
command.withForce();
}
return command;
}
protected parseResult(result: CommandResult): CheckoutResult {
const previousHead = result.stdout.match(/HEAD is now at ([a-f0-9]+)/)?.[1];
const newBranch = result.stdout.includes('Switched to a new branch') ?
this.options.newBranch : undefined;
return {
target: this.options.target,
newBranch,
previousHead,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'checkout',
stateType: RepoStateType.BRANCH
};
}
protected async validateOptions(): Promise<void> {
if (!this.options.target) {
throw ErrorHandler.handleValidationError(
new Error('Checkout target is required'),
{ operation: this.context.operation }
);
}
// Ensure working tree is clean unless force is specified
if (!this.options.force) {
await RepositoryValidator.ensureClean(
this.getResolvedPath(),
this.context.operation
);
}
}
}
```
--------------------------------------------------------------------------------
/src/errors/error-types.ts:
--------------------------------------------------------------------------------
```typescript
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
/**
* Error severity levels for categorizing errors and determining appropriate responses
*/
export const ErrorSeverity = {
CRITICAL: 'CRITICAL', // System-level failures requiring immediate attention
HIGH: 'HIGH', // Operation-blocking errors that need urgent handling
MEDIUM: 'MEDIUM', // Non-blocking errors that should be addressed
LOW: 'LOW' // Minor issues that can be handled gracefully
} as const;
export type ErrorSeverityType = typeof ErrorSeverity[keyof typeof ErrorSeverity];
/**
* Error categories for better error handling and reporting
*/
export const ErrorCategory = {
SYSTEM: 'SYSTEM', // System-level errors (file system, process, etc.)
VALIDATION: 'VALIDATION', // Input validation errors
OPERATION: 'OPERATION', // Git operation errors
REPOSITORY: 'REPOSITORY', // Repository state errors
NETWORK: 'NETWORK', // Network-related errors
CONFIGURATION: 'CONFIG', // Configuration errors
SECURITY: 'SECURITY' // Security-related errors
} as const;
export type ErrorCategoryType = typeof ErrorCategory[keyof typeof ErrorCategory];
/**
* Extended error context for better error tracking and debugging
*/
export interface ErrorContext {
operation: string; // Operation being performed
path?: string; // Path being operated on
command?: string; // Git command being executed
timestamp: number; // Error occurrence timestamp
severity: ErrorSeverityType; // Error severity level
category: ErrorCategoryType; // Error category
details?: {
currentUsage?: number;
threshold?: number;
command?: string;
exitCode?: number | string;
stdout?: string;
stderr?: string;
config?: string;
tool?: string;
args?: unknown;
[key: string]: unknown;
}; // Additional error-specific details
recoverySteps?: string[]; // Suggested recovery steps
stackTrace?: string; // Error stack trace
}
/**
* Base class for all Git MCP server errors
*/
export class GitMcpError extends McpError {
readonly severity: ErrorSeverityType;
readonly category: ErrorCategoryType;
readonly context: ErrorContext;
constructor(
code: ErrorCode,
message: string,
severity: ErrorSeverityType,
category: ErrorCategoryType,
context: Partial<ErrorContext>
) {
super(code, message);
this.name = 'GitMcpError';
this.severity = severity;
this.category = category;
this.context = {
operation: context.operation || 'unknown',
timestamp: Date.now(),
severity,
category,
...context
};
}
/**
* Get recovery steps based on error type and context
*/
getRecoverySteps(): string[] {
return this.context.recoverySteps || this.getDefaultRecoverySteps();
}
/**
* Get default recovery steps based on error category
*/
private getDefaultRecoverySteps(): string[] {
switch (this.category) {
case ErrorCategory.SYSTEM:
return [
'Check system permissions and access rights',
'Verify file system access',
'Check available disk space',
'Ensure required dependencies are installed'
];
case ErrorCategory.VALIDATION:
return [
'Verify input parameters are correct',
'Check path formatting and permissions',
'Ensure all required fields are provided'
];
case ErrorCategory.OPERATION:
return [
'Verify Git command syntax',
'Check repository state',
'Ensure working directory is clean',
'Try running git status for more information'
];
case ErrorCategory.REPOSITORY:
return [
'Verify repository exists and is accessible',
'Check repository permissions',
'Ensure .git directory is intact',
'Try reinitializing the repository'
];
case ErrorCategory.NETWORK:
return [
'Check network connectivity',
'Verify remote repository access',
'Check authentication credentials',
'Try using git remote -v to verify remote configuration'
];
case ErrorCategory.CONFIGURATION:
return [
'Check Git configuration',
'Verify environment variables',
'Ensure required settings are configured',
'Try git config --list to view current configuration'
];
case ErrorCategory.SECURITY:
return [
'Check file and directory permissions',
'Verify authentication credentials',
'Ensure secure connection to remote',
'Review security settings'
];
default:
return [
'Check operation parameters',
'Verify system state',
'Review error message details',
'Contact support if issue persists'
];
}
}
/**
* Format error for logging
*/
toJSON(): Record<string, any> {
return {
name: this.name,
message: this.message,
code: this.code,
severity: this.severity,
category: this.category,
context: this.context,
recoverySteps: this.getRecoverySteps(),
stack: this.stack
};
}
}
/**
* System-level errors
*/
export class SystemError extends GitMcpError {
constructor(message: string, context: Partial<ErrorContext>) {
super(
ErrorCode.InternalError,
message,
ErrorSeverity.CRITICAL,
ErrorCategory.SYSTEM,
context
);
this.name = 'SystemError';
}
}
/**
* Validation errors
*/
export class ValidationError extends GitMcpError {
constructor(message: string, context: Partial<ErrorContext>) {
super(
ErrorCode.InvalidParams,
message,
ErrorSeverity.HIGH,
ErrorCategory.VALIDATION,
context
);
this.name = 'ValidationError';
}
}
/**
* Git operation errors
*/
export class OperationError extends GitMcpError {
constructor(message: string, context: Partial<ErrorContext>) {
super(
ErrorCode.InternalError,
message,
ErrorSeverity.HIGH,
ErrorCategory.OPERATION,
context
);
this.name = 'OperationError';
}
}
/**
* Repository state errors
*/
export class RepositoryError extends GitMcpError {
constructor(message: string, context: Partial<ErrorContext>) {
super(
ErrorCode.InternalError,
message,
ErrorSeverity.HIGH,
ErrorCategory.REPOSITORY,
context
);
this.name = 'RepositoryError';
}
}
/**
* Network-related errors
*/
export class NetworkError extends GitMcpError {
constructor(message: string, context: Partial<ErrorContext>) {
super(
ErrorCode.InternalError,
message,
ErrorSeverity.HIGH,
ErrorCategory.NETWORK,
context
);
this.name = 'NetworkError';
}
}
/**
* Configuration errors
*/
export class ConfigurationError extends GitMcpError {
constructor(message: string, context: Partial<ErrorContext>) {
super(
ErrorCode.InvalidParams,
message,
ErrorSeverity.MEDIUM,
ErrorCategory.CONFIGURATION,
context
);
this.name = 'ConfigurationError';
}
}
/**
* Security-related errors
*/
export class SecurityError extends GitMcpError {
constructor(message: string, context: Partial<ErrorContext>) {
super(
ErrorCode.InternalError,
message,
ErrorSeverity.CRITICAL,
ErrorCategory.SECURITY,
context
);
this.name = 'SecurityError';
}
}
```
--------------------------------------------------------------------------------
/src/operations/working-tree/working-tree-operations.ts:
--------------------------------------------------------------------------------
```typescript
import { BaseGitOperation } from '../base/base-operation.js';
import { GitCommandBuilder } from '../../common/command-builder.js';
import { CommandResult } from '../base/operation-result.js';
import { ErrorHandler } from '../../errors/error-handler.js';
import { RepositoryValidator } from '../../utils/repository.js';
import { CommandExecutor } from '../../utils/command.js';
import { RepoStateType } from '../../caching/repository-cache.js';
import {
AddOptions,
CommitOptions,
StatusOptions,
AddResult,
CommitResult,
StatusResult,
FileChange
} from './working-tree-types.js';
/**
* Handles Git add operations
*/
export class AddOperation extends BaseGitOperation<AddOptions, AddResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.add();
if (this.options.all) {
command.flag('all');
}
if (this.options.update) {
command.flag('update');
}
if (this.options.ignoreRemoval) {
command.flag('no-all');
}
if (this.options.force) {
command.withForce();
}
if (this.options.dryRun) {
command.flag('dry-run');
}
// Add files
this.options.files.forEach(file => command.arg(file));
return command;
}
protected parseResult(result: CommandResult): AddResult {
const staged: string[] = [];
const notStaged: Array<{ path: string; reason: string }> = [];
// Parse output to determine which files were staged
result.stdout.split('\n').forEach(line => {
const match = line.match(/^add '(.+)'$/);
if (match) {
staged.push(match[1]);
} else if (line.includes('error:')) {
const errorMatch = line.match(/error: (.+?) '(.+?)'/);
if (errorMatch) {
notStaged.push({
path: errorMatch[2],
reason: errorMatch[1]
});
}
}
});
return {
staged,
notStaged: notStaged.length > 0 ? notStaged : undefined,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'add',
stateType: RepoStateType.STATUS
};
}
protected validateOptions(): void {
if (!this.options.files || this.options.files.length === 0) {
throw ErrorHandler.handleValidationError(
new Error('At least one file must be specified'),
{ operation: this.context.operation }
);
}
}
}
/**
* Handles Git commit operations
*/
export class CommitOperation extends BaseGitOperation<CommitOptions, CommitResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.commit();
command.withMessage(this.options.message);
if (this.options.allowEmpty) {
command.flag('allow-empty');
}
if (this.options.amend) {
command.flag('amend');
}
if (this.options.noVerify) {
command.withNoVerify();
}
if (this.options.author) {
command.option('author', this.options.author);
}
// Add specific files if provided
if (this.options.files) {
this.options.files.forEach(file => command.arg(file));
}
return command;
}
protected parseResult(result: CommandResult): CommitResult {
const hash = result.stdout.match(/\[.+?(\w+)\]/)?.[1] || '';
const stats = result.stdout.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/);
return {
hash,
filesChanged: stats ? parseInt(stats[1], 10) : 0,
insertions: stats && stats[2] ? parseInt(stats[2], 10) : 0,
deletions: stats && stats[3] ? parseInt(stats[3], 10) : 0,
amended: this.options.amend || false,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'commit',
stateType: RepoStateType.STATUS
};
}
protected async validateOptions(): Promise<void> {
if (!this.options.message && !this.options.amend) {
throw ErrorHandler.handleValidationError(
new Error('Commit message is required unless amending'),
{ operation: this.context.operation }
);
}
// Verify there are staged changes unless allowing empty commits
if (!this.options.allowEmpty) {
const statusResult = await CommandExecutor.executeGitCommand(
'status --porcelain',
this.context.operation,
this.getResolvedPath()
);
if (!statusResult.stdout.trim()) {
throw ErrorHandler.handleValidationError(
new Error('No changes to commit'),
{ operation: this.context.operation }
);
}
}
}
}
/**
* Handles Git status operations
*/
export class StatusOperation extends BaseGitOperation<StatusOptions, StatusResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.status();
// Use porcelain format for consistent parsing
command.flag('porcelain');
command.flag('z'); // Use NUL character as separator
if (this.options.showUntracked) {
command.flag('untracked-files');
}
if (this.options.ignoreSubmodules) {
command.option('ignore-submodules', 'all');
}
if (this.options.showIgnored) {
command.flag('ignored');
}
if (this.options.showBranch) {
command.flag('branch');
}
return command;
}
protected async parseResult(result: CommandResult): Promise<StatusResult> {
const staged: FileChange[] = [];
const unstaged: FileChange[] = [];
const untracked: FileChange[] = [];
const ignored: FileChange[] = [];
// Get current branch
const branchResult = await CommandExecutor.executeGitCommand(
'rev-parse --abbrev-ref HEAD',
this.context.operation,
this.getResolvedPath()
);
const branch = branchResult.stdout.trim();
// Parse status output
const entries = result.stdout.split('\0').filter(Boolean);
for (const entry of entries) {
const [status, ...pathParts] = entry.split(' ');
const path = pathParts.join(' ');
const change: FileChange = {
path,
type: this.parseChangeType(status),
staged: status[0] !== ' ' && status[0] !== '?',
raw: status
};
// Handle renamed files
if (change.type === 'renamed') {
const [oldPath, newPath] = path.split(' -> ');
change.path = newPath;
change.originalPath = oldPath;
}
// Categorize the change
if (status === '??') {
untracked.push(change);
} else if (status === '!!') {
ignored.push(change);
} else if (change.staged) {
staged.push(change);
} else {
unstaged.push(change);
}
}
return {
branch,
clean: staged.length === 0 && unstaged.length === 0 && untracked.length === 0,
staged,
unstaged,
untracked,
ignored: this.options.showIgnored ? ignored : undefined,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'status',
stateType: RepoStateType.STATUS
};
}
protected validateOptions(): void {
// No specific validation needed for status
}
private parseChangeType(status: string): FileChange['type'] {
const index = status[0];
const worktree = status[1];
if (status === '??') return 'untracked';
if (status === '!!') return 'ignored';
if (index === 'R' || worktree === 'R') return 'renamed';
if (index === 'C' || worktree === 'C') return 'copied';
if (index === 'A' || worktree === 'A') return 'added';
if (index === 'D' || worktree === 'D') return 'deleted';
return 'modified';
}
}
```
--------------------------------------------------------------------------------
/src/utils/path.ts:
--------------------------------------------------------------------------------
```typescript
import { resolve, isAbsolute, normalize } from 'path';
import { existsSync, statSync, readdirSync } from 'fs';
import { ErrorHandler } from '../errors/error-handler.js';
import { GitMcpError } from '../errors/error-types.js';
export interface PathValidationOptions {
mustExist?: boolean;
allowDirectory?: boolean;
allowPattern?: boolean;
cwd?: string;
operation?: string;
}
export class PathValidator {
static validatePath(path: string, options: PathValidationOptions = {}): string {
const {
mustExist = true,
allowDirectory = true,
cwd = process.cwd(),
operation = 'path_validation'
} = options;
try {
if (!path || typeof path !== 'string') {
throw new Error('Path must be a non-empty string');
}
// Convert to absolute path if relative
const absolutePath = isAbsolute(path) ? normalize(path) : resolve(cwd, path);
// Check existence if required
if (mustExist && !existsSync(absolutePath)) {
throw new Error(`Path does not exist: ${path}`);
}
// If path exists and is not a pattern, validate type
if (existsSync(absolutePath)) {
const stats = statSync(absolutePath);
if (!allowDirectory && stats.isDirectory()) {
throw new Error(`Path is a directory when file expected: ${path}`);
}
}
return absolutePath;
} catch (error: unknown) {
throw ErrorHandler.handleValidationError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: { options }
});
}
}
static validateGitRepo(path: string, operation = 'validate_repo'): { path: string; hasEmbeddedRepo: boolean } {
try {
const absolutePath = this.validatePath(path, { allowDirectory: true, operation });
const gitPath = resolve(absolutePath, '.git');
if (!existsSync(gitPath)) {
throw new Error(`Not a git repository: ${path}`);
}
if (!statSync(gitPath).isDirectory()) {
throw new Error(`Invalid git repository: ${path}`);
}
// Check for embedded repositories
let hasEmbeddedRepo = false;
const checkEmbeddedRepos = (dir: string) => {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const fullPath = resolve(dir, entry.name);
if (entry.name === '.git' && fullPath !== gitPath) {
hasEmbeddedRepo = true;
break;
}
if (entry.name !== '.git' && entry.name !== 'node_modules') {
checkEmbeddedRepos(fullPath);
}
}
}
};
checkEmbeddedRepos(absolutePath);
return { path: absolutePath, hasEmbeddedRepo };
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: { gitPath: resolve(path, '.git') }
});
}
}
static validatePaths(paths: string[], options: PathValidationOptions = {}): string[] {
const {
allowPattern = false,
cwd = process.cwd(),
operation = 'validate_paths'
} = options;
try {
if (!Array.isArray(paths)) {
throw new Error('Paths must be an array');
}
return paths.map(path => {
if (!path || typeof path !== 'string') {
throw new Error('Each path must be a non-empty string');
}
// If patterns are allowed and path contains wildcards, return as-is
if (allowPattern && /[*?[\]]/.test(path)) {
return path;
}
// For relative paths starting with '.', make them relative to the repository root
if (path.startsWith('.')) {
// Just return the path as-is to let Git handle it relative to the repo root
return path;
}
// Convert to absolute path if relative
return isAbsolute(path) ? normalize(path) : resolve(cwd, path);
});
} catch (error: unknown) {
throw ErrorHandler.handleValidationError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
details: { paths, options }
});
}
}
static validateBranchName(branch: string, operation = 'validate_branch'): void {
try {
if (!branch || typeof branch !== 'string') {
throw new Error('Branch name must be a non-empty string');
}
// Git branch naming rules
if (!/^(?!\/|\.|\.\.|@|\{|\}|\[|\]|\\)[\x21-\x7E]+(?<!\.lock|[/.])$/.test(branch)) {
throw new Error('Invalid branch name format');
}
} catch (error: unknown) {
throw ErrorHandler.handleValidationError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
details: { branch }
});
}
}
static validateRemoteName(name: string, operation = 'validate_remote'): void {
try {
if (!name || typeof name !== 'string') {
throw new Error('Remote name must be a non-empty string');
}
// Git remote naming rules
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(name)) {
throw new Error('Invalid remote name format');
}
} catch (error: unknown) {
throw ErrorHandler.handleValidationError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
details: { remoteName: name }
});
}
}
static validateRemoteUrl(url: string, operation = 'validate_remote_url'): void {
try {
if (!url || typeof url !== 'string') {
throw new Error('Remote URL must be a non-empty string');
}
// Basic URL format validation for git URLs
const gitUrlPattern = /^(git|https?|ssh):\/\/|^git@|^[a-zA-Z0-9_-]+:/;
if (!gitUrlPattern.test(url)) {
throw new Error('Invalid git remote URL format');
}
// Additional security checks for URLs
const securityPattern = /[<>'";&|]/;
if (securityPattern.test(url)) {
throw new Error('Remote URL contains invalid characters');
}
} catch (error: unknown) {
throw ErrorHandler.handleValidationError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
details: {
url,
allowedProtocols: ['git', 'https', 'ssh']
}
});
}
}
static validateTagName(tag: string, operation = 'validate_tag'): void {
try {
if (!tag || typeof tag !== 'string') {
throw new Error('Tag name must be a non-empty string');
}
// Git tag naming rules
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(tag)) {
throw new Error('Invalid tag name format');
}
// Additional validation for semantic versioning tags
if (tag.startsWith('v') && !/^v\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(tag)) {
throw new Error('Invalid semantic version tag format');
}
} catch (error: unknown) {
throw ErrorHandler.handleValidationError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
details: {
tag,
semanticVersioning: tag.startsWith('v')
}
});
}
}
/**
* Validates a commit message format
*/
static validateCommitMessage(message: string, operation = 'validate_commit'): void {
try {
if (!message || typeof message !== 'string') {
throw new Error('Commit message must be a non-empty string');
}
// Basic commit message format validation
if (message.length > 72) {
throw new Error('Commit message exceeds maximum length of 72 characters');
}
// Check for conventional commit format if it appears to be one
const conventionalPattern = /^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .+/;
if (message.match(/^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)/) && !conventionalPattern.test(message)) {
throw new Error('Invalid conventional commit format');
}
} catch (error: unknown) {
throw ErrorHandler.handleValidationError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
details: {
message,
isConventionalCommit: message.match(/^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)/) !== null
}
});
}
}
}
```
--------------------------------------------------------------------------------
/src/utils/repository.ts:
--------------------------------------------------------------------------------
```typescript
import { existsSync } from 'fs';
import { join } from 'path';
import { CommandExecutor } from './command.js';
import { ErrorHandler } from '../errors/error-handler.js';
import { GitMcpError } from '../errors/error-types.js';
export class RepositoryValidator {
/**
* Get list of configured remotes
*/
static async getRemotes(path: string, operation: string): Promise<string[]> {
const result = await CommandExecutor.executeGitCommand(
'remote',
operation,
path
);
return result.stdout.split('\n').filter(Boolean);
}
static async validateLocalRepo(path: string, operation: string): Promise<void> {
try {
const gitDir = join(path, '.git');
if (!existsSync(gitDir)) {
throw new Error('Not a git repository');
}
} catch (error: unknown) {
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: { gitDir: join(path, '.git') }
});
}
}
static async validateRemoteRepo(remote: string, operation: string): Promise<void> {
try {
await CommandExecutor.execute(`git ls-remote ${remote}`, operation);
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
details: {
remote,
action: 'validate_remote_repo'
}
});
}
}
static async validateBranchExists(path: string, branch: string, operation: string): Promise<void> {
try {
await CommandExecutor.execute(
`git show-ref --verify --quiet refs/heads/${branch}`,
operation,
path
);
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: {
branch,
action: 'validate_branch_exists'
}
});
}
}
static async validateRemoteBranchExists(path: string, remote: string, branch: string, operation: string): Promise<void> {
try {
await CommandExecutor.execute(
`git show-ref --verify --quiet refs/remotes/${remote}/${branch}`,
operation,
path
);
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: {
remote,
branch,
action: 'validate_remote_branch_exists'
}
});
}
}
static async getCurrentBranch(path: string, operation: string): Promise<string> {
try {
const result = await CommandExecutor.execute('git rev-parse --abbrev-ref HEAD', operation, path);
return result.stdout.trim();
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: {
action: 'get_current_branch',
command: 'git rev-parse --abbrev-ref HEAD'
}
});
}
}
static async ensureClean(path: string, operation: string): Promise<void> {
let statusResult;
try {
statusResult = await CommandExecutor.execute('git status --porcelain', operation, path);
if (statusResult.stdout.trim()) {
throw new Error('Working directory is not clean. Please commit or stash your changes.');
}
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: {
action: 'ensure_clean',
status: statusResult?.stdout || 'unknown'
}
});
}
}
static async validateRemoteConfig(path: string, remote: string, operation: string): Promise<void> {
let remoteResult;
try {
remoteResult = await CommandExecutor.execute(`git remote get-url ${remote}`, operation, path);
if (!remoteResult.stdout.trim()) {
throw new Error(`Remote ${remote} is not configured`);
}
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: {
remote,
action: 'validate_remote_config',
remoteUrl: remoteResult?.stdout || 'unknown'
}
});
}
}
static async validateCommitExists(path: string, commit: string, operation: string): Promise<void> {
try {
await CommandExecutor.execute(`git cat-file -e ${commit}^{commit}`, operation, path);
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: {
commit,
action: 'validate_commit_exists'
}
});
}
}
static async validateTagExists(path: string, tag: string, operation: string): Promise<void> {
try {
await CommandExecutor.execute(`git show-ref --tags --quiet refs/tags/${tag}`, operation, path);
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: {
tag,
action: 'validate_tag_exists'
}
});
}
}
/**
* Validates repository configuration
*/
static async validateRepositoryConfig(path: string, operation: string): Promise<void> {
let configResult;
try {
// Check core configuration
configResult = await CommandExecutor.execute('git config --list', operation, path);
const config = new Map<string, string>(
configResult.stdout
.split('\n')
.filter(line => line)
.map(line => {
const [key, ...values] = line.split('=');
return [key, values.join('=')] as [string, string];
})
);
// Required configurations
const requiredConfigs = [
['core.repositoryformatversion', '0'],
['core.filemode', 'true'],
['core.bare', 'false']
];
for (const [key, value] of requiredConfigs) {
if (config.get(key) !== value) {
throw new Error(`Invalid repository configuration: ${key}=${config.get(key) || 'undefined'}`);
}
}
// Check repository integrity
await CommandExecutor.execute('git fsck --full', operation, path);
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: {
action: 'validate_repository_config',
config: configResult?.stdout || 'unknown'
}
});
}
}
/**
* Checks if a repository has any uncommitted changes
*/
static async hasUncommittedChanges(path: string, operation: string): Promise<boolean> {
try {
const result = await CommandExecutor.execute('git status --porcelain', operation, path);
return result.stdout.trim().length > 0;
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: {
action: 'check_uncommitted_changes'
}
});
}
}
/**
* Gets the repository's current state information
*/
static async getRepositoryState(path: string, operation: string): Promise<{
branch: string;
isClean: boolean;
hasStashed: boolean;
remotes: string[];
lastCommit: string;
}> {
try {
const [branch, isClean, stashList, remoteList, lastCommit] = await Promise.all([
this.getCurrentBranch(path, operation),
this.hasUncommittedChanges(path, operation).then(changes => !changes),
CommandExecutor.execute('git stash list', operation, path),
CommandExecutor.execute('git remote', operation, path),
CommandExecutor.execute('git log -1 --format=%H', operation, path)
]);
return {
branch,
isClean,
hasStashed: stashList.stdout.trim().length > 0,
remotes: remoteList.stdout.trim().split('\n').filter(Boolean),
lastCommit: lastCommit.stdout.trim()
};
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleRepositoryError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
details: {
action: 'get_repository_state'
}
});
}
}
}
```
--------------------------------------------------------------------------------
/src/operations/remote/remote-operations.ts:
--------------------------------------------------------------------------------
```typescript
import { BaseGitOperation } from '../base/base-operation.js';
import { GitCommandBuilder } from '../../common/command-builder.js';
import { CommandResult } from '../base/operation-result.js';
import { ErrorHandler } from '../../errors/error-handler.js';
import { RepositoryValidator } from '../../utils/repository.js';
import { CommandExecutor } from '../../utils/command.js';
import { RepoStateType } from '../../caching/repository-cache.js';
import {
RemoteListOptions,
RemoteAddOptions,
RemoteRemoveOptions,
RemoteSetUrlOptions,
RemotePruneOptions,
RemoteListResult,
RemoteAddResult,
RemoteRemoveResult,
RemoteSetUrlResult,
RemotePruneResult,
RemoteConfig
} from './remote-types.js';
/**
* Handles Git remote listing operations
*/
export class RemoteListOperation extends BaseGitOperation<RemoteListOptions, RemoteListResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.remote();
if (this.options.verbose) {
command.flag('verbose');
}
return command;
}
protected async parseResult(result: CommandResult): Promise<RemoteListResult> {
const remotes: RemoteConfig[] = [];
const lines = result.stdout.split('\n').filter(Boolean);
for (const line of lines) {
const [name, url, purpose] = line.split(/\s+/);
// Find or create remote config
let remote = remotes.find(r => r.name === name);
if (!remote) {
remote = {
name,
fetchUrl: url
};
remotes.push(remote);
}
// Set URL based on purpose
if (purpose === '(push)') {
remote.pushUrl = url;
}
// Get additional configuration if verbose
if (this.options.verbose) {
const configResult = await CommandExecutor.executeGitCommand(
`config --get-regexp ^remote\\.${name}\\.`,
this.context.operation,
this.getResolvedPath()
);
configResult.stdout.split('\n').filter(Boolean).forEach(configLine => {
const [key, value] = configLine.split(' ');
const configKey = key.split('.')[2];
switch (configKey) {
case 'tagopt':
remote!.fetchTags = value === '--tags';
break;
case 'mirror':
remote!.mirror = value as 'fetch' | 'push';
break;
case 'fetch':
if (!remote!.branches) remote!.branches = [];
const branch = value.match(/refs\/heads\/(.+):refs\/remotes\/.+/)?.[1];
if (branch) remote!.branches.push(branch);
break;
}
});
}
}
return {
remotes,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'remote',
stateType: RepoStateType.REMOTE
};
}
protected validateOptions(): void {
// No specific validation needed for listing
}
}
/**
* Handles Git remote add operations
*/
export class RemoteAddOperation extends BaseGitOperation<RemoteAddOptions, RemoteAddResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.remote()
.arg('add');
if (this.options.fetch) {
command.flag('fetch');
}
if (typeof this.options.tags === 'boolean') {
command.flag(this.options.tags ? 'tags' : 'no-tags');
}
if (this.options.mirror) {
command.option('mirror', this.options.mirror);
}
command.arg(this.options.name)
.arg(this.options.url);
return command;
}
protected async parseResult(result: CommandResult): Promise<RemoteAddResult> {
// Get full remote configuration
const listOperation = new RemoteListOperation(this.context, { verbose: true });
const listResult = await listOperation.execute();
const remotes = listResult.data?.remotes;
if (!remotes) {
throw ErrorHandler.handleOperationError(
new Error('Failed to get remote list'),
{ operation: this.context.operation }
);
}
const remote = remotes.find(r => r.name === this.options.name);
if (!remote) {
throw ErrorHandler.handleOperationError(
new Error(`Failed to get configuration for remote ${this.options.name}`),
{ operation: this.context.operation }
);
}
return {
remote,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'remote_add',
stateType: RepoStateType.REMOTE
};
}
protected validateOptions(): void {
if (!this.options.name) {
throw ErrorHandler.handleValidationError(
new Error('Remote name is required'),
{ operation: this.context.operation }
);
}
if (!this.options.url) {
throw ErrorHandler.handleValidationError(
new Error('Remote URL is required'),
{ operation: this.context.operation }
);
}
}
}
/**
* Handles Git remote remove operations
*/
export class RemoteRemoveOperation extends BaseGitOperation<RemoteRemoveOptions, RemoteRemoveResult> {
protected buildCommand(): GitCommandBuilder {
return GitCommandBuilder.remote()
.arg('remove')
.arg(this.options.name);
}
protected parseResult(result: CommandResult): RemoteRemoveResult {
return {
name: this.options.name,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'remote_remove',
stateType: RepoStateType.REMOTE
};
}
protected async validateOptions(): Promise<void> {
if (!this.options.name) {
throw ErrorHandler.handleValidationError(
new Error('Remote name is required'),
{ operation: this.context.operation }
);
}
// Ensure remote exists
await RepositoryValidator.validateRemoteConfig(
this.getResolvedPath(),
this.options.name,
this.context.operation
);
}
}
/**
* Handles Git remote set-url operations
*/
export class RemoteSetUrlOperation extends BaseGitOperation<RemoteSetUrlOptions, RemoteSetUrlResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.remote()
.arg('set-url');
if (this.options.pushUrl) {
command.flag('push');
}
if (this.options.add) {
command.flag('add');
}
if (this.options.delete) {
command.flag('delete');
}
command.arg(this.options.name)
.arg(this.options.url);
return command;
}
protected async parseResult(result: CommandResult): Promise<RemoteSetUrlResult> {
// Get full remote configuration
const listOperation = new RemoteListOperation(this.context, { verbose: true });
const listResult = await listOperation.execute();
const remotes = listResult.data?.remotes;
if (!remotes) {
throw ErrorHandler.handleOperationError(
new Error('Failed to get remote list'),
{ operation: this.context.operation }
);
}
const remote = remotes.find(r => r.name === this.options.name);
if (!remote) {
throw ErrorHandler.handleOperationError(
new Error(`Failed to get configuration for remote ${this.options.name}`),
{ operation: this.context.operation }
);
}
return {
remote,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'remote_set_url',
stateType: RepoStateType.REMOTE
};
}
protected async validateOptions(): Promise<void> {
if (!this.options.name) {
throw ErrorHandler.handleValidationError(
new Error('Remote name is required'),
{ operation: this.context.operation }
);
}
if (!this.options.url) {
throw ErrorHandler.handleValidationError(
new Error('Remote URL is required'),
{ operation: this.context.operation }
);
}
// Ensure remote exists
await RepositoryValidator.validateRemoteConfig(
this.getResolvedPath(),
this.options.name,
this.context.operation
);
}
}
/**
* Handles Git remote prune operations
*/
export class RemotePruneOperation extends BaseGitOperation<RemotePruneOptions, RemotePruneResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.remote()
.arg('prune');
if (this.options.dryRun) {
command.flag('dry-run');
}
command.arg(this.options.name);
return command;
}
protected parseResult(result: CommandResult): RemotePruneResult {
const prunedBranches = result.stdout
.split('\n')
.filter(line => line.includes('* [pruned] '))
.map(line => line.match(/\* \[pruned\] (.+)/)?.[1] || '');
return {
name: this.options.name,
prunedBranches,
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'remote_prune',
stateType: RepoStateType.REMOTE
};
}
protected async validateOptions(): Promise<void> {
if (!this.options.name) {
throw ErrorHandler.handleValidationError(
new Error('Remote name is required'),
{ operation: this.context.operation }
);
}
// Ensure remote exists
await RepositoryValidator.validateRemoteConfig(
this.getResolvedPath(),
this.options.name,
this.context.operation
);
}
}
```
--------------------------------------------------------------------------------
/src/monitoring/performance.ts:
--------------------------------------------------------------------------------
```typescript
import { logger } from '../utils/logger.js';
import { ErrorHandler } from '../errors/error-handler.js';
import { PerformanceError } from './types.js';
import { ErrorCategory, ErrorSeverity } from '../errors/error-types.js';
/**
* Performance metric types
*/
export enum MetricType {
OPERATION_DURATION = 'operation_duration',
MEMORY_USAGE = 'memory_usage',
COMMAND_EXECUTION = 'command_execution',
CACHE_HIT = 'cache_hit',
CACHE_MISS = 'cache_miss',
RESOURCE_USAGE = 'resource_usage'
}
/**
* Performance metric data structure
*/
export interface Metric {
type: MetricType;
value: number;
timestamp: number;
labels: Record<string, string>;
context?: Record<string, any>;
}
/**
* Resource usage thresholds
*/
export interface ResourceThresholds {
memory: {
warning: number; // MB
critical: number; // MB
};
cpu: {
warning: number; // Percentage
critical: number; // Percentage
};
operations: {
warning: number; // Operations per second
critical: number; // Operations per second
};
}
/**
* Default resource thresholds
*/
const DEFAULT_THRESHOLDS: ResourceThresholds = {
memory: {
warning: 1024, // 1GB
critical: 2048 // 2GB
},
cpu: {
warning: 70, // 70%
critical: 90 // 90%
},
operations: {
warning: 100, // 100 ops/sec
critical: 200 // 200 ops/sec
}
};
/**
* Performance monitoring system
*/
export class PerformanceMonitor {
private static instance: PerformanceMonitor;
private metrics: Metric[] = [];
private thresholds: ResourceThresholds;
private operationTimers: Map<string, number> = new Map();
private readonly METRICS_RETENTION = 3600; // 1 hour in seconds
private readonly METRICS_CLEANUP_INTERVAL = 300; // 5 minutes in seconds
private constructor() {
this.thresholds = DEFAULT_THRESHOLDS;
this.startMetricsCleanup();
}
/**
* Get singleton instance
*/
static getInstance(): PerformanceMonitor {
if (!PerformanceMonitor.instance) {
PerformanceMonitor.instance = new PerformanceMonitor();
}
return PerformanceMonitor.instance;
}
/**
* Start operation timing
*/
startOperation(operation: string): void {
this.operationTimers.set(operation, performance.now());
}
/**
* End operation timing and record metric
*/
endOperation(operation: string, context?: Record<string, any>): void {
const startTime = this.operationTimers.get(operation);
if (!startTime) {
logger.warn(operation, 'No start time found for operation timing', undefined, new Error('Missing operation start time'));
return;
}
const duration = performance.now() - startTime;
this.operationTimers.delete(operation);
this.recordMetric({
type: MetricType.OPERATION_DURATION,
value: duration,
timestamp: Date.now(),
labels: { operation },
context
});
}
/**
* Record command execution metric
*/
recordCommandExecution(command: string, duration: number, context?: Record<string, any>): void {
this.recordMetric({
type: MetricType.COMMAND_EXECUTION,
value: duration,
timestamp: Date.now(),
labels: { command },
context
});
}
/**
* Record memory usage metric
*/
recordMemoryUsage(context?: Record<string, any>): void {
const memoryUsage = process.memoryUsage();
const memoryUsageMB = memoryUsage.heapUsed / 1024 / 1024; // Convert to MB
// Record heap usage
this.recordMetric({
type: MetricType.MEMORY_USAGE,
value: memoryUsageMB,
timestamp: Date.now(),
labels: { type: 'heap' },
context: {
...context,
heapTotal: memoryUsage.heapTotal / 1024 / 1024,
external: memoryUsage.external / 1024 / 1024,
rss: memoryUsage.rss / 1024 / 1024
}
});
// Check thresholds
this.checkMemoryThresholds(memoryUsageMB);
}
/**
* Record resource usage metric
*/
recordResourceUsage(
resource: string,
value: number,
context?: Record<string, any>
): void {
this.recordMetric({
type: MetricType.RESOURCE_USAGE,
value,
timestamp: Date.now(),
labels: { resource },
context
});
}
/**
* Record cache hit/miss
*/
recordCacheAccess(hit: boolean, cacheType: string, context?: Record<string, any>): void {
this.recordMetric({
type: hit ? MetricType.CACHE_HIT : MetricType.CACHE_MISS,
value: 1,
timestamp: Date.now(),
labels: { cacheType },
context
});
}
/**
* Get metrics for a specific type and time range
*/
getMetrics(
type: MetricType,
startTime: number,
endTime: number = Date.now()
): Metric[] {
return this.metrics.filter(metric =>
metric.type === type &&
metric.timestamp >= startTime &&
metric.timestamp <= endTime
);
}
/**
* Calculate operation rate (operations per second)
*/
getOperationRate(operation: string, windowSeconds: number = 60): number {
const now = Date.now();
const startTime = now - (windowSeconds * 1000);
const operationMetrics = this.getMetrics(
MetricType.OPERATION_DURATION,
startTime,
now
).filter(metric => metric.labels.operation === operation);
return operationMetrics.length / windowSeconds;
}
/**
* Get average operation duration
*/
getAverageOperationDuration(
operation: string,
windowSeconds: number = 60
): number {
const now = Date.now();
const startTime = now - (windowSeconds * 1000);
const operationMetrics = this.getMetrics(
MetricType.OPERATION_DURATION,
startTime,
now
).filter(metric => metric.labels.operation === operation);
if (operationMetrics.length === 0) return 0;
const totalDuration = operationMetrics.reduce(
(sum, metric) => sum + metric.value,
0
);
return totalDuration / operationMetrics.length;
}
/**
* Get cache hit rate
*/
getCacheHitRate(cacheType: string, windowSeconds: number = 60): number {
const now = Date.now();
const startTime = now - (windowSeconds * 1000);
const hits = this.getMetrics(MetricType.CACHE_HIT, startTime, now)
.filter(metric => metric.labels.cacheType === cacheType).length;
const misses = this.getMetrics(MetricType.CACHE_MISS, startTime, now)
.filter(metric => metric.labels.cacheType === cacheType).length;
const total = hits + misses;
return total === 0 ? 0 : hits / total;
}
/**
* Update resource thresholds
*/
updateThresholds(thresholds: Partial<ResourceThresholds>): void {
this.thresholds = {
...this.thresholds,
...thresholds
};
}
/**
* Get current thresholds
*/
getThresholds(): ResourceThresholds {
return { ...this.thresholds };
}
/**
* Private helper to record a metric
*/
private recordMetric(metric: Metric): void {
this.metrics.push(metric);
// Log high severity metrics
if (
metric.type === MetricType.MEMORY_USAGE ||
metric.type === MetricType.RESOURCE_USAGE
) {
const metricError = new PerformanceError(
`Recorded ${metric.type} metric`,
{
details: {
value: metric.value,
labels: metric.labels,
context: metric.context
},
operation: metric.labels.operation || 'performance'
}
);
logger.info(
metric.labels.operation || 'performance',
`Recorded ${metric.type} metric`,
undefined,
metricError
);
}
}
/**
* Check memory usage against thresholds
*/
private checkMemoryThresholds(memoryUsageMB: number): void {
if (memoryUsageMB >= this.thresholds.memory.critical) {
const error = new PerformanceError(
`Critical memory usage: ${memoryUsageMB.toFixed(2)}MB`,
{
details: {
currentUsage: memoryUsageMB,
threshold: this.thresholds.memory.critical
},
operation: 'memory_monitor',
severity: ErrorSeverity.CRITICAL,
category: ErrorCategory.SYSTEM
}
);
ErrorHandler.handleSystemError(error, {
operation: 'memory_monitor',
severity: ErrorSeverity.CRITICAL,
category: ErrorCategory.SYSTEM
});
} else if (memoryUsageMB >= this.thresholds.memory.warning) {
const warningError = new PerformanceError(
`High memory usage: ${memoryUsageMB.toFixed(2)}MB`,
{
details: {
currentUsage: memoryUsageMB,
threshold: this.thresholds.memory.warning
},
operation: 'memory_monitor'
}
);
logger.warn(
'memory_monitor',
`High memory usage: ${memoryUsageMB.toFixed(2)}MB`,
undefined,
warningError
);
}
}
/**
* Start periodic metrics cleanup
*/
private startMetricsCleanup(): void {
setInterval(() => {
const cutoffTime = Date.now() - (this.METRICS_RETENTION * 1000);
this.metrics = this.metrics.filter(metric => metric.timestamp >= cutoffTime);
}, this.METRICS_CLEANUP_INTERVAL * 1000);
}
/**
* Get current performance statistics
*/
getStatistics(): Record<string, any> {
const now = Date.now();
const oneMinuteAgo = now - 60000;
const fiveMinutesAgo = now - 300000;
return {
memory: {
current: process.memoryUsage().heapUsed / 1024 / 1024,
trend: this.getMetrics(MetricType.MEMORY_USAGE, fiveMinutesAgo)
.map(m => ({ timestamp: m.timestamp, value: m.value }))
},
operations: {
last1m: this.metrics
.filter(m =>
m.type === MetricType.OPERATION_DURATION &&
m.timestamp >= oneMinuteAgo
).length,
last5m: this.metrics
.filter(m =>
m.type === MetricType.OPERATION_DURATION &&
m.timestamp >= fiveMinutesAgo
).length
},
cache: {
hitRate1m: this.getCacheHitRate('all', 60),
hitRate5m: this.getCacheHitRate('all', 300)
},
commandExecutions: {
last1m: this.metrics
.filter(m =>
m.type === MetricType.COMMAND_EXECUTION &&
m.timestamp >= oneMinuteAgo
).length,
last5m: this.metrics
.filter(m =>
m.type === MetricType.COMMAND_EXECUTION &&
m.timestamp >= fiveMinutesAgo
).length
}
};
}
}
```
--------------------------------------------------------------------------------
/src/operations/sync/sync-operations.ts:
--------------------------------------------------------------------------------
```typescript
import { BaseGitOperation } from '../base/base-operation.js';
import { GitCommandBuilder } from '../../common/command-builder.js';
import { CommandResult } from '../base/operation-result.js';
import { ErrorHandler } from '../../errors/error-handler.js';
import { RepositoryValidator } from '../../utils/repository.js';
import { CommandExecutor } from '../../utils/command.js';
import { RepoStateType } from '../../caching/repository-cache.js';
import {
PushOptions,
PullOptions,
FetchOptions,
PushResult,
PullResult,
FetchResult
} from './sync-types.js';
/**
* Handles Git push operations
*/
export class PushOperation extends BaseGitOperation<PushOptions, PushResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.push();
if (this.options.remote) {
command.arg(this.options.remote);
}
if (this.options.branch) {
command.arg(this.options.branch);
}
if (this.options.force) {
command.withForce();
}
if (this.options.forceWithLease) {
command.flag('force-with-lease');
}
if (this.options.all) {
command.flag('all');
}
if (this.options.tags) {
command.flag('tags');
}
if (this.options.noVerify) {
command.withNoVerify();
}
if (this.options.setUpstream) {
command.withSetUpstream();
}
if (this.options.prune) {
command.flag('prune');
}
return command;
}
protected parseResult(result: CommandResult): PushResult {
const summary = {
created: [] as string[],
deleted: [] as string[],
updated: [] as string[],
rejected: [] as string[]
};
// Parse push output
result.stdout.split('\n').forEach(line => {
if (line.startsWith('To ')) return; // Skip remote URL line
const match = line.match(/^\s*([a-f0-9]+)\.\.([a-f0-9]+)\s+(\S+)\s+->\s+(\S+)/);
if (match) {
const [, oldRef, newRef, localRef, remoteRef] = match;
summary.updated.push(remoteRef);
} else if (line.includes('[new branch]')) {
const branchMatch = line.match(/\[new branch\]\s+(\S+)\s+->\s+(\S+)/);
if (branchMatch) {
summary.created.push(branchMatch[2]);
}
} else if (line.includes('[deleted]')) {
const deleteMatch = line.match(/\[deleted\]\s+(\S+)/);
if (deleteMatch) {
summary.deleted.push(deleteMatch[1]);
}
} else if (line.includes('! [rejected]')) {
const rejectMatch = line.match(/\! \[rejected\]\s+(\S+)/);
if (rejectMatch) {
summary.rejected.push(rejectMatch[1]);
}
}
});
return {
remote: this.options.remote || 'origin',
branch: this.options.branch,
forced: this.options.force || false,
summary: {
created: summary.created.length > 0 ? summary.created : undefined,
deleted: summary.deleted.length > 0 ? summary.deleted : undefined,
updated: summary.updated.length > 0 ? summary.updated : undefined,
rejected: summary.rejected.length > 0 ? summary.rejected : undefined
},
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'push',
stateType: RepoStateType.REMOTE
};
}
protected async validateOptions(): Promise<void> {
if (!this.options.branch && !this.options.all) {
throw ErrorHandler.handleValidationError(
new Error('Either branch or --all must be specified'),
{ operation: this.context.operation }
);
}
if (this.options.remote) {
await RepositoryValidator.validateRemoteConfig(
this.getResolvedPath(),
this.options.remote,
this.context.operation
);
}
if (this.options.branch) {
await RepositoryValidator.validateBranchExists(
this.getResolvedPath(),
this.options.branch,
this.context.operation
);
}
}
}
/**
* Handles Git pull operations
*/
export class PullOperation extends BaseGitOperation<PullOptions, PullResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.pull();
if (this.options.remote) {
command.arg(this.options.remote);
}
if (this.options.branch) {
command.arg(this.options.branch);
}
if (this.options.rebase) {
command.flag('rebase');
}
if (this.options.autoStash) {
command.flag('autostash');
}
if (this.options.allowUnrelated) {
command.flag('allow-unrelated-histories');
}
if (this.options.ff === 'only') {
command.flag('ff-only');
} else if (this.options.ff === 'no') {
command.flag('no-ff');
}
if (this.options.strategy) {
command.option('strategy', this.options.strategy);
}
if (this.options.strategyOption) {
this.options.strategyOption.forEach(opt => {
command.option('strategy-option', opt);
});
}
return command;
}
protected parseResult(result: CommandResult): PullResult {
const summary = {
merged: [] as string[],
conflicts: [] as string[]
};
let filesChanged = 0;
let insertions = 0;
let deletions = 0;
// Parse pull output
result.stdout.split('\n').forEach(line => {
if (line.includes('|')) {
// Parse merge stats
const statsMatch = line.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/);
if (statsMatch) {
filesChanged = parseInt(statsMatch[1], 10);
insertions = statsMatch[2] ? parseInt(statsMatch[2], 10) : 0;
deletions = statsMatch[3] ? parseInt(statsMatch[3], 10) : 0;
}
} else if (line.includes('Fast-forward') || line.includes('Merge made by')) {
// Track merged files
const mergeMatch = line.match(/([^/]+)$/);
if (mergeMatch) {
summary.merged.push(mergeMatch[1]);
}
} else if (line.includes('CONFLICT')) {
// Track conflicts
const conflictMatch = line.match(/CONFLICT \(.+?\): (.+)/);
if (conflictMatch) {
summary.conflicts.push(conflictMatch[1]);
}
}
});
return {
remote: this.options.remote || 'origin',
branch: this.options.branch,
rebased: this.options.rebase || false,
filesChanged,
insertions,
deletions,
summary: {
merged: summary.merged.length > 0 ? summary.merged : undefined,
conflicts: summary.conflicts.length > 0 ? summary.conflicts : undefined
},
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'pull',
stateType: RepoStateType.REMOTE
};
}
protected async validateOptions(): Promise<void> {
if (!this.options.branch) {
throw ErrorHandler.handleValidationError(
new Error('Branch must be specified'),
{ operation: this.context.operation }
);
}
if (this.options.remote) {
await RepositoryValidator.validateRemoteConfig(
this.getResolvedPath(),
this.options.remote,
this.context.operation
);
}
// Ensure working tree is clean unless autostash is enabled
if (!this.options.autoStash) {
await RepositoryValidator.ensureClean(
this.getResolvedPath(),
this.context.operation
);
}
}
}
/**
* Handles Git fetch operations
*/
export class FetchOperation extends BaseGitOperation<FetchOptions, FetchResult> {
protected buildCommand(): GitCommandBuilder {
const command = GitCommandBuilder.fetch();
if (this.options.remote && !this.options.all) {
command.arg(this.options.remote);
}
if (this.options.all) {
command.flag('all');
}
if (this.options.prune) {
command.flag('prune');
}
if (this.options.pruneTags) {
command.flag('prune-tags');
}
if (this.options.tags) {
command.flag('tags');
}
if (this.options.tagsOnly) {
command.flag('tags').flag('no-recurse-submodules');
}
if (this.options.forceTags) {
command.flag('force').flag('tags');
}
if (this.options.depth) {
command.option('depth', this.options.depth.toString());
}
if (typeof this.options.recurseSubmodules !== 'undefined') {
if (typeof this.options.recurseSubmodules === 'boolean') {
command.flag(this.options.recurseSubmodules ? 'recurse-submodules' : 'no-recurse-submodules');
} else {
command.option('recurse-submodules', this.options.recurseSubmodules);
}
}
if (this.options.progress) {
command.flag('progress');
}
return command;
}
protected parseResult(result: CommandResult): FetchResult {
const summary = {
branches: [] as Array<{ name: string; oldRef?: string; newRef: string }>,
tags: [] as Array<{ name: string; oldRef?: string; newRef: string }>,
pruned: [] as string[]
};
// Parse fetch output
result.stdout.split('\n').forEach(line => {
if (line.includes('->')) {
// Parse branch/tag updates
const match = line.match(/([a-f0-9]+)\.\.([a-f0-9]+)\s+(\S+)\s+->\s+(\S+)/);
if (match) {
const [, oldRef, newRef, localRef, remoteRef] = match;
if (remoteRef.includes('refs/tags/')) {
summary.tags.push({
name: remoteRef.replace('refs/tags/', ''),
oldRef,
newRef
});
} else {
summary.branches.push({
name: remoteRef.replace('refs/remotes/', ''),
oldRef,
newRef
});
}
}
} else if (line.includes('[pruned]')) {
// Parse pruned refs
const pruneMatch = line.match(/\[pruned\] (.+)/);
if (pruneMatch) {
summary.pruned.push(pruneMatch[1]);
}
}
});
return {
remote: this.options.remote,
summary: {
branches: summary.branches.length > 0 ? summary.branches : undefined,
tags: summary.tags.length > 0 ? summary.tags : undefined,
pruned: summary.pruned.length > 0 ? summary.pruned : undefined
},
raw: result.stdout
};
}
protected getCacheConfig() {
return {
command: 'fetch',
stateType: RepoStateType.REMOTE
};
}
protected async validateOptions(): Promise<void> {
if (this.options.remote && !this.options.all) {
await RepositoryValidator.validateRemoteConfig(
this.getResolvedPath(),
this.options.remote,
this.context.operation
);
}
if (this.options.depth !== undefined && this.options.depth <= 0) {
throw ErrorHandler.handleValidationError(
new Error('Depth must be a positive number'),
{ operation: this.context.operation }
);
}
}
}
```
--------------------------------------------------------------------------------
/src/tool-handler.ts:
--------------------------------------------------------------------------------
```typescript
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { GitOperations } from './git-operations.js';
import { logger } from './utils/logger.js';
import { ErrorHandler } from './errors/error-handler.js';
import { GitMcpError } from './errors/error-types.js';
import {
isInitOptions,
isCloneOptions,
isAddOptions,
isCommitOptions,
isPushPullOptions,
isBranchOptions,
isCheckoutOptions,
isTagOptions,
isRemoteOptions,
isStashOptions,
isPathOnly,
isBulkActionOptions,
BasePathOptions,
} from './types.js';
const PATH_DESCRIPTION = `MUST be an absolute path (e.g., /Users/username/projects/my-repo)`;
const FILE_PATH_DESCRIPTION = `MUST be an absolute path (e.g., /Users/username/projects/my-repo/src/file.js)`;
export class ToolHandler {
private static readonly TOOL_PREFIX = 'git_mcp_server';
constructor(private server: Server) {
this.setupHandlers();
}
private getOperationName(toolName: string): string {
return `${ToolHandler.TOOL_PREFIX}.${toolName}`;
}
private validateArguments<T extends BasePathOptions>(operation: string, args: unknown, validator: (obj: any) => obj is T): T {
if (!args || !validator(args)) {
throw ErrorHandler.handleValidationError(
new Error(`Invalid arguments for operation: ${operation}`),
{
operation,
details: { args }
}
);
}
// If path is not provided, use default path from environment
if (!args.path && process.env.GIT_DEFAULT_PATH) {
args.path = process.env.GIT_DEFAULT_PATH;
logger.info(operation, 'Using default git path', args.path);
}
return args;
}
private setupHandlers(): void {
this.setupToolDefinitions();
this.setupToolExecutor();
}
private setupToolDefinitions(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'init',
description: 'Initialize a new Git repository',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to initialize the repository in. ${PATH_DESCRIPTION}`,
},
},
required: [],
},
},
{
name: 'clone',
description: 'Clone a repository',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL of the repository to clone',
},
path: {
type: 'string',
description: `Path to clone into. ${PATH_DESCRIPTION}`,
},
},
required: ['url'],
},
},
{
name: 'status',
description: 'Get repository status',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
},
required: [],
},
},
{
name: 'add',
description: 'Stage files',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
files: {
type: 'array',
items: {
type: 'string',
description: FILE_PATH_DESCRIPTION,
},
description: 'Files to stage',
},
},
required: ['files'],
},
},
{
name: 'commit',
description: 'Create a commit',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
message: {
type: 'string',
description: 'Commit message',
},
},
required: ['message'],
},
},
{
name: 'push',
description: 'Push commits to remote',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
remote: {
type: 'string',
description: 'Remote name',
default: 'origin',
},
branch: {
type: 'string',
description: 'Branch name',
},
force: {
type: 'boolean',
description: 'Force push changes',
default: false
},
noVerify: {
type: 'boolean',
description: 'Skip pre-push hooks',
default: false
},
tags: {
type: 'boolean',
description: 'Push all tags',
default: false
}
},
required: ['branch'],
},
},
{
name: 'pull',
description: 'Pull changes from remote',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
remote: {
type: 'string',
description: 'Remote name',
default: 'origin',
},
branch: {
type: 'string',
description: 'Branch name',
},
},
required: ['branch'],
},
},
{
name: 'branch_list',
description: 'List all branches',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
},
required: [],
},
},
{
name: 'branch_create',
description: 'Create a new branch',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
name: {
type: 'string',
description: 'Branch name',
},
force: {
type: 'boolean',
description: 'Force create branch even if it exists',
default: false
},
track: {
type: 'boolean',
description: 'Set up tracking mode',
default: true
},
setUpstream: {
type: 'boolean',
description: 'Set upstream for push/pull',
default: false
}
},
required: ['name'],
},
},
{
name: 'branch_delete',
description: 'Delete a branch',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
name: {
type: 'string',
description: 'Branch name',
},
},
required: ['name'],
},
},
{
name: 'checkout',
description: 'Switch branches or restore working tree files',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
target: {
type: 'string',
description: 'Branch name, commit hash, or file path',
},
},
required: ['target'],
},
},
{
name: 'tag_list',
description: 'List tags',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
},
required: [],
},
},
{
name: 'tag_create',
description: 'Create a tag',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
name: {
type: 'string',
description: 'Tag name',
},
message: {
type: 'string',
description: 'Tag message',
},
force: {
type: 'boolean',
description: 'Force create tag even if it exists',
default: false
},
annotated: {
type: 'boolean',
description: 'Create an annotated tag',
default: true
},
sign: {
type: 'boolean',
description: 'Create a signed tag',
default: false
}
},
required: ['name'],
},
},
{
name: 'tag_delete',
description: 'Delete a tag',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
name: {
type: 'string',
description: 'Tag name',
},
},
required: ['name'],
},
},
{
name: 'remote_list',
description: 'List remotes',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
},
required: [],
},
},
{
name: 'remote_add',
description: 'Add a remote',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
name: {
type: 'string',
description: 'Remote name',
},
url: {
type: 'string',
description: 'Remote URL',
},
},
required: ['name', 'url'],
},
},
{
name: 'remote_remove',
description: 'Remove a remote',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
name: {
type: 'string',
description: 'Remote name',
},
},
required: ['name'],
},
},
{
name: 'stash_list',
description: 'List stashes',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
},
required: [],
},
},
{
name: 'stash_save',
description: 'Save changes to stash',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
message: {
type: 'string',
description: 'Stash message',
},
includeUntracked: {
type: 'boolean',
description: 'Include untracked files',
default: false
},
keepIndex: {
type: 'boolean',
description: 'Keep staged changes',
default: false
},
all: {
type: 'boolean',
description: 'Include ignored files',
default: false
}
},
required: [],
},
},
{
name: 'stash_pop',
description: 'Apply and remove a stash',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
index: {
type: 'number',
description: 'Stash index',
default: 0,
},
},
required: [],
},
},
// New bulk action tool
{
name: 'bulk_action',
description: 'Execute multiple Git operations in sequence. This is the preferred way to execute multiple operations.',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: `Path to repository. ${PATH_DESCRIPTION}`,
},
actions: {
type: 'array',
description: 'Array of Git operations to execute in sequence',
items: {
type: 'object',
oneOf: [
{
type: 'object',
properties: {
type: { const: 'stage' },
files: {
type: 'array',
items: {
type: 'string',
description: FILE_PATH_DESCRIPTION,
},
description: 'Files to stage. If not provided, stages all changes.',
},
},
required: ['type'],
},
{
type: 'object',
properties: {
type: { const: 'commit' },
message: {
type: 'string',
description: 'Commit message',
},
},
required: ['type', 'message'],
},
{
type: 'object',
properties: {
type: { const: 'push' },
remote: {
type: 'string',
description: 'Remote name',
default: 'origin',
},
branch: {
type: 'string',
description: 'Branch name',
},
},
required: ['type', 'branch'],
},
],
},
minItems: 1,
},
},
required: ['actions'],
},
},
],
}));
}
private setupToolExecutor(): void {
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const operation = this.getOperationName(request.params.name);
const args = request.params.arguments;
const context = { operation, path: args?.path as string | undefined };
try {
switch (request.params.name) {
case 'init': {
const validArgs = this.validateArguments(operation, args, isInitOptions);
return await GitOperations.init(validArgs, context);
}
case 'clone': {
const validArgs = this.validateArguments(operation, args, isCloneOptions);
return await GitOperations.clone(validArgs, context);
}
case 'status': {
const validArgs = this.validateArguments(operation, args, isPathOnly);
return await GitOperations.status(validArgs, context);
}
case 'add': {
const validArgs = this.validateArguments(operation, args, isAddOptions);
return await GitOperations.add(validArgs, context);
}
case 'commit': {
const validArgs = this.validateArguments(operation, args, isCommitOptions);
return await GitOperations.commit(validArgs, context);
}
case 'push': {
const validArgs = this.validateArguments(operation, args, isPushPullOptions);
return await GitOperations.push(validArgs, context);
}
case 'pull': {
const validArgs = this.validateArguments(operation, args, isPushPullOptions);
return await GitOperations.pull(validArgs, context);
}
case 'branch_list': {
const validArgs = this.validateArguments(operation, args, isPathOnly);
return await GitOperations.branchList(validArgs, context);
}
case 'branch_create': {
const validArgs = this.validateArguments(operation, args, isBranchOptions);
return await GitOperations.branchCreate(validArgs, context);
}
case 'branch_delete': {
const validArgs = this.validateArguments(operation, args, isBranchOptions);
return await GitOperations.branchDelete(validArgs, context);
}
case 'checkout': {
const validArgs = this.validateArguments(operation, args, isCheckoutOptions);
return await GitOperations.checkout(validArgs, context);
}
case 'tag_list': {
const validArgs = this.validateArguments(operation, args, isPathOnly);
return await GitOperations.tagList(validArgs, context);
}
case 'tag_create': {
const validArgs = this.validateArguments(operation, args, isTagOptions);
return await GitOperations.tagCreate(validArgs, context);
}
case 'tag_delete': {
const validArgs = this.validateArguments(operation, args, isTagOptions);
return await GitOperations.tagDelete(validArgs, context);
}
case 'remote_list': {
const validArgs = this.validateArguments(operation, args, isPathOnly);
return await GitOperations.remoteList(validArgs, context);
}
case 'remote_add': {
const validArgs = this.validateArguments(operation, args, isRemoteOptions);
return await GitOperations.remoteAdd(validArgs, context);
}
case 'remote_remove': {
const validArgs = this.validateArguments(operation, args, isRemoteOptions);
return await GitOperations.remoteRemove(validArgs, context);
}
case 'stash_list': {
const validArgs = this.validateArguments(operation, args, isPathOnly);
return await GitOperations.stashList(validArgs, context);
}
case 'stash_save': {
const validArgs = this.validateArguments(operation, args, isStashOptions);
return await GitOperations.stashSave(validArgs, context);
}
case 'stash_pop': {
const validArgs = this.validateArguments(operation, args, isStashOptions);
return await GitOperations.stashPop(validArgs, context);
}
case 'bulk_action': {
const validArgs = this.validateArguments(operation, args, isBulkActionOptions);
return await GitOperations.executeBulkActions(validArgs, context);
}
default:
throw ErrorHandler.handleValidationError(
new Error(`Unknown tool: ${request.params.name}`),
{ operation }
);
}
} catch (error: unknown) {
// If it's already a GitMcpError or McpError, rethrow it
if (error instanceof GitMcpError || error instanceof McpError) {
throw error;
}
// Otherwise, wrap it in an appropriate error type
throw ErrorHandler.handleOperationError(
error instanceof Error ? error : new Error('Unknown error'),
{
operation,
path: context.path,
details: { tool: request.params.name }
}
);
}
});
}
}
```
--------------------------------------------------------------------------------
/src/git-operations.ts:
--------------------------------------------------------------------------------
```typescript
import { CommandExecutor } from './utils/command.js';
import { PathValidator } from './utils/path.js';
import { RepositoryValidator } from './utils/repository.js';
import { logger } from './utils/logger.js';
import { repositoryCache } from './caching/repository-cache.js';
import { RepoStateType } from './caching/repository-cache.js';
import {
GitToolResult,
GitToolContext,
InitOptions,
CloneOptions,
AddOptions,
CommitOptions,
PushPullOptions,
BranchOptions,
CheckoutOptions,
TagOptions,
RemoteOptions,
StashOptions,
BasePathOptions,
BulkActionOptions,
BulkAction,
} from './types.js';
import { resolve } from 'path';
import { existsSync } from 'fs';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { ErrorHandler } from './errors/error-handler.js';
import { GitMcpError } from './errors/error-types.js';
export class GitOperations {
private static async executeOperation<T>(
operation: string,
path: string | undefined,
action: () => Promise<T>,
options: {
useCache?: boolean;
stateType?: RepoStateType;
command?: string;
invalidateCache?: boolean;
} = {}
): Promise<T> {
try {
logger.info(operation, 'Starting git operation', path);
let result: T;
if (options.useCache && path && options.stateType && options.command) {
// Use cache for repository state operations
result = await repositoryCache.getState(
path,
options.stateType,
options.command,
action
);
} else if (options.useCache && path && options.command) {
// Use cache for command results
result = await repositoryCache.getCommandResult(
path,
options.command,
action
);
} else {
// Execute without caching
result = await action();
}
// Invalidate cache if needed
if (options.invalidateCache && path) {
if (options.stateType) {
repositoryCache.invalidateState(path, options.stateType);
}
if (options.command) {
repositoryCache.invalidateCommand(path, options.command);
}
}
logger.info(operation, 'Operation completed successfully', path);
return result;
} catch (error: unknown) {
if (error instanceof GitMcpError) throw error;
throw ErrorHandler.handleOperationError(error instanceof Error ? error : new Error('Unknown error'), {
operation,
path,
command: options.command || 'git operation'
});
}
}
private static getPath(options: BasePathOptions): string {
if (!options.path && !process.env.GIT_DEFAULT_PATH) {
throw ErrorHandler.handleValidationError(
new Error('Path must be provided when GIT_DEFAULT_PATH is not set'),
{ operation: 'get_path' }
);
}
return options.path || process.env.GIT_DEFAULT_PATH!;
}
static async init(options: InitOptions, context: GitToolContext): Promise<GitToolResult> {
const path = this.getPath(options);
return await this.executeOperation(
context.operation,
path,
async () => {
const pathInfo = PathValidator.validatePath(path, { mustExist: false, allowDirectory: true });
const result = await CommandExecutor.executeGitCommand(
'init',
context.operation,
pathInfo
);
return {
content: [{
type: 'text',
text: `Repository initialized successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'init',
invalidateCache: true // Invalidate all caches for this repo
}
);
}
static async clone(options: CloneOptions, context: GitToolContext): Promise<GitToolResult> {
const path = this.getPath(options);
return await this.executeOperation(
context.operation,
path,
async () => {
const pathInfo = PathValidator.validatePath(path, { mustExist: false, allowDirectory: true });
const result = await CommandExecutor.executeGitCommand(
`clone ${options.url} ${pathInfo}`,
context.operation
);
return {
content: [{
type: 'text',
text: `Repository cloned successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'clone',
invalidateCache: true // Invalidate all caches for this repo
}
);
}
static async status(options: BasePathOptions, context: GitToolContext): Promise<GitToolResult> {
const path = this.getPath(options);
return await this.executeOperation(
context.operation,
path,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(path);
const result = await CommandExecutor.executeGitCommand(
'status',
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: CommandExecutor.formatOutput(result)
}]
};
},
{
useCache: true,
stateType: RepoStateType.STATUS,
command: 'status'
}
);
}
static async add({ path, files }: AddOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
// Handle each file individually to avoid path issues
for (const file of files) {
await CommandExecutor.executeGitCommand(
`add "${file}"`,
context.operation,
repoPath
);
}
return {
content: [{
type: 'text',
text: 'Files staged successfully'
}]
};
},
{
command: 'add',
invalidateCache: true, // Invalidate status cache
stateType: RepoStateType.STATUS
}
);
}
static async commit({ path, message }: CommitOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
// Verify there are staged changes
const statusResult = await CommandExecutor.executeGitCommand(
'status --porcelain',
context.operation,
repoPath
);
if (!statusResult.stdout.trim()) {
return {
content: [{
type: 'text',
text: 'No changes to commit'
}],
isError: true
};
}
const result = await CommandExecutor.executeGitCommand(
`commit -m "${message}"`,
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: `Changes committed successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'commit',
invalidateCache: true, // Invalidate status and branch caches
stateType: RepoStateType.STATUS
}
);
}
static async push({ path, remote = 'origin', branch, force, noVerify, tags }: PushPullOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
await RepositoryValidator.validateRemoteConfig(repoPath, remote, context.operation);
await RepositoryValidator.validateBranchExists(repoPath, branch, context.operation);
const result = await CommandExecutor.executeGitCommand(
`push ${remote} ${branch}${force ? ' --force' : ''}${noVerify ? ' --no-verify' : ''}${tags ? ' --tags' : ''}`,
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: `Changes pushed successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'push',
invalidateCache: true, // Invalidate remote cache
stateType: RepoStateType.REMOTE
}
);
}
static async executeBulkActions(options: BulkActionOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath(options);
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
const results: string[] = [];
for (const action of options.actions) {
try {
switch (action.type) {
case 'stage': {
const files = action.files || ['.'];
const addResult = await this.add({ path: repoPath, files }, context);
results.push(addResult.content[0].text);
break;
}
case 'commit': {
const commitResult = await this.commit({ path: repoPath, message: action.message }, context);
results.push(commitResult.content[0].text);
break;
}
case 'push': {
const pushResult = await this.push({
path: repoPath,
remote: action.remote,
branch: action.branch
}, context);
results.push(pushResult.content[0].text);
break;
}
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
results.push(`Failed to execute ${action.type}: ${errorMessage}`);
if (error instanceof Error) {
logger.error(context.operation, `Bulk action ${action.type} failed`, repoPath, error);
}
}
}
return {
content: [{
type: 'text',
text: results.join('\n\n')
}]
};
},
{
command: 'bulk_action',
invalidateCache: true // Invalidate all caches
}
);
}
static async pull({ path, remote = 'origin', branch }: PushPullOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
await RepositoryValidator.validateRemoteConfig(repoPath, remote, context.operation);
const result = await CommandExecutor.executeGitCommand(
`pull ${remote} ${branch}`,
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: `Changes pulled successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'pull',
invalidateCache: true // Invalidate all caches
}
);
}
static async branchList(options: BasePathOptions, context: GitToolContext): Promise<GitToolResult> {
const path = this.getPath(options);
return await this.executeOperation(
context.operation,
path,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(path);
const result = await CommandExecutor.executeGitCommand(
'branch -a',
context.operation,
repoPath
);
const output = result.stdout.trim();
return {
content: [{
type: 'text',
text: output || 'No branches found'
}]
};
},
{
useCache: true,
stateType: RepoStateType.BRANCH,
command: 'branch -a'
}
);
}
static async branchCreate({ path, name, force, track, setUpstream }: BranchOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
PathValidator.validateBranchName(name);
const result = await CommandExecutor.executeGitCommand(
`checkout -b ${name}${force ? ' --force' : ''}${track ? ' --track' : ' --no-track'}${setUpstream ? ' --set-upstream' : ''}`,
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: `Branch '${name}' created successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'branch_create',
invalidateCache: true, // Invalidate branch cache
stateType: RepoStateType.BRANCH
}
);
}
static async branchDelete({ path, name }: BranchOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
PathValidator.validateBranchName(name);
await RepositoryValidator.validateBranchExists(repoPath, name, context.operation);
const currentBranch = await RepositoryValidator.getCurrentBranch(repoPath, context.operation);
if (currentBranch === name) {
throw ErrorHandler.handleValidationError(
new Error(`Cannot delete the currently checked out branch: ${name}`),
{ operation: context.operation, path: repoPath }
);
}
const result = await CommandExecutor.executeGitCommand(
`branch -D ${name}`,
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: `Branch '${name}' deleted successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'branch_delete',
invalidateCache: true, // Invalidate branch cache
stateType: RepoStateType.BRANCH
}
);
}
static async checkout({ path, target }: CheckoutOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
await RepositoryValidator.ensureClean(repoPath, context.operation);
const result = await CommandExecutor.executeGitCommand(
`checkout ${target}`,
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: `Switched to '${target}' successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'checkout',
invalidateCache: true, // Invalidate branch and status caches
stateType: RepoStateType.BRANCH
}
);
}
static async tagList(options: BasePathOptions, context: GitToolContext): Promise<GitToolResult> {
const path = this.getPath(options);
return await this.executeOperation(
context.operation,
path,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(path);
const result = await CommandExecutor.executeGitCommand(
'tag -l',
context.operation,
repoPath
);
const output = result.stdout.trim();
return {
content: [{
type: 'text',
text: output || 'No tags found'
}]
};
},
{
useCache: true,
stateType: RepoStateType.TAG,
command: 'tag -l'
}
);
}
static async tagCreate({ path, name, message }: TagOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
PathValidator.validateTagName(name);
let command = `tag ${name}`;
if (typeof message === 'string' && message.length > 0) {
command = `tag -a ${name} -m "${message}"`;
}
const result = await CommandExecutor.executeGitCommand(
command,
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: `Tag '${name}' created successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'tag_create',
invalidateCache: true, // Invalidate tag cache
stateType: RepoStateType.TAG
}
);
}
static async tagDelete({ path, name }: TagOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
PathValidator.validateTagName(name);
await RepositoryValidator.validateTagExists(repoPath, name, context.operation);
const result = await CommandExecutor.executeGitCommand(
`tag -d ${name}`,
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: `Tag '${name}' deleted successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'tag_delete',
invalidateCache: true, // Invalidate tag cache
stateType: RepoStateType.TAG
}
);
}
static async remoteList(options: BasePathOptions, context: GitToolContext): Promise<GitToolResult> {
const path = this.getPath(options);
return await this.executeOperation(
context.operation,
path,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(path);
const result = await CommandExecutor.executeGitCommand(
'remote -v',
context.operation,
repoPath
);
const output = result.stdout.trim();
return {
content: [{
type: 'text',
text: output || 'No remotes configured'
}]
};
},
{
useCache: true,
stateType: RepoStateType.REMOTE,
command: 'remote -v'
}
);
}
static async remoteAdd({ path, name, url }: RemoteOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
PathValidator.validateRemoteName(name);
if (!url) {
throw ErrorHandler.handleValidationError(
new Error('URL is required when adding a remote'),
{ operation: context.operation, path: repoPath }
);
}
PathValidator.validateRemoteUrl(url);
const result = await CommandExecutor.executeGitCommand(
`remote add ${name} ${url}`,
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: `Remote '${name}' added successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'remote_add',
invalidateCache: true, // Invalidate remote cache
stateType: RepoStateType.REMOTE
}
);
}
static async remoteRemove({ path, name }: RemoteOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
PathValidator.validateRemoteName(name);
const result = await CommandExecutor.executeGitCommand(
`remote remove ${name}`,
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: `Remote '${name}' removed successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'remote_remove',
invalidateCache: true, // Invalidate remote cache
stateType: RepoStateType.REMOTE
}
);
}
static async stashList(options: BasePathOptions, context: GitToolContext): Promise<GitToolResult> {
const path = this.getPath(options);
return await this.executeOperation(
context.operation,
path,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(path);
const result = await CommandExecutor.executeGitCommand(
'stash list',
context.operation,
repoPath
);
const output = result.stdout.trim();
return {
content: [{
type: 'text',
text: output || 'No stashes found'
}]
};
},
{
useCache: true,
stateType: RepoStateType.STASH,
command: 'stash list'
}
);
}
static async stashSave({ path, message, includeUntracked, keepIndex, all }: StashOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
let command = 'stash';
if (typeof message === 'string' && message.length > 0) {
command += ` save "${message}"`;
}
if (includeUntracked) {
command += ' --include-untracked';
}
if (keepIndex) {
command += ' --keep-index';
}
if (all) {
command += ' --all';
}
const result = await CommandExecutor.executeGitCommand(
command,
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: `Changes stashed successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'stash_save',
invalidateCache: true, // Invalidate stash and status caches
stateType: RepoStateType.STASH
}
);
}
static async stashPop({ path, index = 0 }: StashOptions, context: GitToolContext): Promise<GitToolResult> {
const resolvedPath = this.getPath({ path });
return await this.executeOperation(
context.operation,
resolvedPath,
async () => {
const { path: repoPath } = PathValidator.validateGitRepo(resolvedPath);
const result = await CommandExecutor.executeGitCommand(
`stash pop stash@{${index}}`,
context.operation,
repoPath
);
return {
content: [{
type: 'text',
text: `Stash applied successfully\n${CommandExecutor.formatOutput(result)}`
}]
};
},
{
command: 'stash_pop',
invalidateCache: true, // Invalidate stash and status caches
stateType: RepoStateType.STASH
}
);
}
}
```