Skip to content

Commit e156564

Browse files
authored
Merge branch 'main' into docs/add-mkdocs-and-align-philosophy
2 parents 1f5da7a + 6f09a5a commit e156564

File tree

9 files changed

+436
-5
lines changed

9 files changed

+436
-5
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## [4.4.1](https://github.com/jonmatum/git-metrics-mcp-server/compare/v4.4.0...v4.4.1) (2025-11-26)
4+
5+
6+
### Bug Fixes
7+
8+
* lower coverage thresholds for CI environment compatibility ([25484ea](https://github.com/jonmatum/git-metrics-mcp-server/commit/25484eac652c1f29090a7d7a9374386b681d1f99))
9+
10+
## [4.4.0](https://github.com/jonmatum/git-metrics-mcp-server/compare/v4.3.1...v4.4.0) (2025-11-26)
11+
12+
13+
### Features
14+
15+
* add production-ready features and 80%+ test coverage ([21d64e2](https://github.com/jonmatum/git-metrics-mcp-server/commit/21d64e2a136641899088f768a801809e782cbd3e))
16+
317
## [4.3.1](https://github.com/jonmatum/git-metrics-mcp-server/compare/v4.3.0...v4.3.1) (2025-11-26)
418

519

CONTRIBUTING.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Contributing to Git Metrics MCP Server
2+
3+
## Development Setup
4+
5+
1. **Clone and install**
6+
```bash
7+
git clone https://github.com/jonmatum/git-metrics-mcp-server.git
8+
cd git-metrics-mcp-server
9+
npm install
10+
```
11+
12+
2. **Build the project**
13+
```bash
14+
npm run build
15+
```
16+
17+
3. **Run in development mode**
18+
```bash
19+
npm run dev
20+
```
21+
22+
## Running Tests
23+
24+
```bash
25+
# Run all tests
26+
npm test
27+
28+
# Run tests in watch mode
29+
npm run test:watch
30+
31+
# Run tests with coverage
32+
npm run test:coverage
33+
```
34+
35+
Coverage thresholds are enforced at 80% for lines/functions/statements and 75% for branches.
36+
37+
## Making Changes
38+
39+
1. **Create a feature branch**
40+
```bash
41+
git checkout -b feature/your-feature-name
42+
```
43+
44+
2. **Make your changes**
45+
- Write tests for new functionality
46+
- Ensure all tests pass: `npm test`
47+
- Follow existing code style (TypeScript strict mode)
48+
- Add structured logging for important operations
49+
50+
3. **Commit your changes**
51+
- Use conventional commits format:
52+
- `feat:` for new features
53+
- `fix:` for bug fixes
54+
- `docs:` for documentation
55+
- `test:` for test changes
56+
- `refactor:` for code refactoring
57+
58+
Example: `feat: add new metric for code complexity`
59+
60+
4. **Push and create PR**
61+
```bash
62+
git push origin feature/your-feature-name
63+
```
64+
- Open a Pull Request on GitHub
65+
- Describe your changes clearly
66+
- Link any related issues
67+
68+
## Code Guidelines
69+
70+
- Use TypeScript with strict mode
71+
- Validate all user inputs
72+
- Sanitize inputs to prevent command injection
73+
- Add error handling with descriptive messages
74+
- Use structured JSON logging: `log('INFO', 'message', { metadata })`
75+
- Write tests for new features and bug fixes
76+
77+
## Project Structure
78+
79+
```
80+
src/
81+
├── git-metrics.ts # Main server and core functions
82+
├── handlers.ts # Tool implementation handlers
83+
└── *.test.ts # Test files
84+
```
85+
86+
## Questions?
87+
88+
Open an issue on GitHub or reach out to the maintainers.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@jonmatum/git-metrics-mcp-server",
3-
"version": "4.3.1",
3+
"version": "4.4.1",
44
"description": "MCP server for analyzing git repository metrics and understanding team health",
55
"main": "./build/git-metrics.js",
66
"type": "module",

src/core-functions.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2+
import { runGitCommand, sanitizeInput, validateDate, validateRepoPath } from './git-metrics.js';
3+
import { execSync } from 'child_process';
4+
import { mkdirSync, writeFileSync, rmSync } from 'fs';
5+
import { resolve } from 'path';
6+
7+
const TEST_REPO = resolve(process.cwd(), 'test-repo-core');
8+
9+
beforeAll(() => {
10+
rmSync(TEST_REPO, { recursive: true, force: true });
11+
mkdirSync(TEST_REPO, { recursive: true });
12+
execSync('git init', { cwd: TEST_REPO });
13+
execSync('git config user.email "test@example.com"', { cwd: TEST_REPO });
14+
execSync('git config user.name "Test User"', { cwd: TEST_REPO });
15+
writeFileSync(resolve(TEST_REPO, 'test.txt'), 'test');
16+
execSync('git add .', { cwd: TEST_REPO });
17+
execSync('git commit -m "test"', { cwd: TEST_REPO });
18+
});
19+
20+
afterAll(() => {
21+
rmSync(TEST_REPO, { recursive: true, force: true });
22+
});
23+
24+
describe('Core Functions', () => {
25+
describe('runGitCommand', () => {
26+
it('should execute git commands', () => {
27+
const result = runGitCommand(TEST_REPO, 'git log --oneline');
28+
expect(result).toContain('test');
29+
});
30+
31+
it('should throw on non-existent repo', () => {
32+
expect(() => runGitCommand('/nonexistent', 'git log')).toThrow('does not exist');
33+
});
34+
});
35+
36+
describe('sanitizeInput', () => {
37+
it('should remove dangerous characters', () => {
38+
expect(sanitizeInput('test;rm -rf')).toBe('testrm -rf');
39+
expect(sanitizeInput('test&whoami')).toBe('testwhoami');
40+
expect(sanitizeInput('test|cat')).toBe('testcat');
41+
expect(sanitizeInput('test`ls`')).toBe('testls');
42+
expect(sanitizeInput('test$HOME')).toBe('testHOME');
43+
expect(sanitizeInput('test()')).toBe('test');
44+
});
45+
46+
it('should keep safe characters', () => {
47+
expect(sanitizeInput('test-file_123.txt')).toBe('test-file_123.txt');
48+
});
49+
});
50+
51+
describe('validateDate', () => {
52+
it('should accept valid dates', () => {
53+
expect(() => validateDate('2025-11-26', 'test')).not.toThrow();
54+
expect(() => validateDate('2020-01-01', 'test')).not.toThrow();
55+
});
56+
57+
it('should reject invalid formats', () => {
58+
expect(() => validateDate('2025/11/26', 'test')).toThrow('Invalid test format');
59+
expect(() => validateDate('11-26-2025', 'test')).toThrow('Invalid test format');
60+
expect(() => validateDate('invalid', 'test')).toThrow('Invalid test format');
61+
});
62+
});
63+
64+
describe('validateRepoPath', () => {
65+
it('should accept valid repo paths', () => {
66+
expect(() => validateRepoPath(TEST_REPO)).not.toThrow();
67+
});
68+
69+
it('should reject empty paths', () => {
70+
expect(() => validateRepoPath('')).toThrow('repo_path is required');
71+
});
72+
73+
it('should reject non-string paths', () => {
74+
expect(() => validateRepoPath(null as any)).toThrow('repo_path is required');
75+
});
76+
77+
it('should reject paths with dangerous characters', () => {
78+
expect(() => validateRepoPath('test;rm')).toThrow('Invalid characters');
79+
});
80+
81+
it('should reject non-existent paths', () => {
82+
expect(() => validateRepoPath('/nonexistent')).toThrow('does not exist');
83+
});
84+
85+
it('should reject non-git directories', () => {
86+
const nonGitDir = resolve(process.cwd(), 'test-non-git');
87+
mkdirSync(nonGitDir, { recursive: true });
88+
expect(() => validateRepoPath(nonGitDir)).toThrow('Not a git repository');
89+
rmSync(nonGitDir, { recursive: true, force: true });
90+
});
91+
});
92+
});

src/git-metrics.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ export function validateRepoPath(repoPath: string): void {
8484

8585
server.setRequestHandler(ListToolsRequestSchema, async () => ({
8686
tools: [
87+
{
88+
name: "health_check",
89+
description: "Verify server is operational",
90+
inputSchema: {
91+
type: "object",
92+
properties: {},
93+
},
94+
},
8795
{
8896
name: "get_commit_stats",
8997
description: "Get commit statistics for a repository",
@@ -242,7 +250,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
242250
const args = request.params.arguments as any;
243251
let result: any;
244252

245-
if (request.params.name === "get_commit_stats") {
253+
if (request.params.name === "health_check") {
254+
result = { status: "ok", version: VERSION, timestamp: new Date().toISOString() };
255+
} else if (request.params.name === "get_commit_stats") {
246256
result = handlers.handleGetCommitStats(args);
247257
} else if (request.params.name === "get_author_metrics") {
248258
result = handlers.handleGetAuthorMetrics(args);
@@ -297,6 +307,19 @@ async function main() {
297307
timeout: GIT_TIMEOUT,
298308
maxBuffer: MAX_BUFFER
299309
});
310+
311+
// Graceful shutdown
312+
process.on('SIGTERM', async () => {
313+
log('INFO', 'Shutting down gracefully');
314+
await server.close();
315+
process.exit(0);
316+
});
317+
318+
process.on('SIGINT', async () => {
319+
log('INFO', 'Shutting down gracefully');
320+
await server.close();
321+
process.exit(0);
322+
});
300323
}
301324

302325
main().catch((error) => {

0 commit comments

Comments
 (0)