✨ 공부하며 성장하는 기록 공간입니다.
아직 부족한 점이 많고 배워야 할 것도 많지만, 그만큼 배우는 즐거움도 큽니다.
틀리거나 부족한 내용이 있다면 언제든지 편하게 지적해 주세요 😁
이 블로그는 오픈소스 프로젝트 githru-vscode-extension을 공부하고 기여한 내용을 정리한 공간입니다.
처음보다 성장해 있는 나 자신을 기대하며 꾸준히 기록해나가겠습니다!
Githru-VSCode-Extension
Githru는 Git 커밋 데이터를 기반으로 개발자의 작업 패턴, 코드 변경 흐름, 팀 협업 패턴 등을 직관적인 시각화로 제공하는
데이터 시각화 분석 도구로 이를 통해 개발 팀의 생산성을 확인할 수 있고, 협업 효율성을 높이는 것을 목표로 합니다.
다들 Pull Request에 대해서 작업은 해보셨을겁니다. Pull Request를 보낼 때에 다른 개발자들이 내가 작업한 코드를 파악하고
리뷰를 남겨주는데, 이는 소프트웨어 품질 보장과 지식 공유에 있어 핵심 프로세스입니다.
하지만 제가 생각했을 때에 다음과 같은 불편함이 있다고 생각했습니다.
- 적절한 리뷰어 선택의 어려움: 수정 파일에 대해 잘 알고 있는 사람에 대한 무지.
- 컨텍스트 부족: 변경된 코드의 히스토리와 관련된 전문가 파악이 어려움.
- 시간 소모: 리뷰를 요청하기 전에는 리뷰 달리는 시간이 오래 걸림.
따라서 저는 이런 문제를 해결하기 위해, Git 정보를 편리하게 파악할 수 있는 Githru를 통해 더 명확하게 리뷰어를 제공하면 어떨까 싶었고, MCP를 통해 Claude와 같은 AI 어시스턴트와 통합하여 자연어로 리뷰어를 추천받을 수 있는 ContributorRecommender Tool을 개발하기로 하였습니다!!🎉
MCP Server Setting
CreateServer
MCP Server는 앞서 말했듯이 Model Context Protocol을 통해 Claude Desktop과 통신하는 독립적인 서버입니다.
Githru MCP 서버는 Git 저장소 분석 도구를 제공하여, Claude가 Github 데이터에 접근하고 분석할 수 있도록 합니다.
(언어는 Githru가 TypeScript 기반이다 보니, 코드의 유지보수성을 위해 TypeScript로 작성하였습니다!)
서버 생성 프로세스
// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// 1. MCP 서버 인스턴스 생성
const server = new McpServer({
name: "githru-mcp",
version: "0.0.1",
});
// 2. 도구 등록
registerAnalysisTools(server);
// 3. Transport 연결
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err) => {
console.error("Server error:", err);
process.exit(1);
});
STDIO Transport
STDIO(Standard Input/Output) 방식은 로컬 개발 환경에서 가장 일반적으로 사용되는 통신 방식입니다.
저는 다음과 같은 특징을 통해 로컬에서 STDIO MCP Server를 개발하게 되었습니다.
🔹특징
- 간단한 설정: 표준 입출력을 통한 통신으로 추가 네트워크 설정 불필요
- 로컬 개발에 적합: 개발 및 디버깅이 용이
🔹설정예시
{
"mcpServers": {
"githru-mcp": {
"command": "node",
"args": ["/path/to/dist/server.js"],
"env": {
"GITHUB_TOKEN": "your_token_here"
}
}
}
}
- Claude Desktop은 claude_desktop_config.json을 통해 MCP Server의 설정을 할 수 있습니다.
- Mac OS 기준: ~/Library/Application Support/Claude/claude_desktop_config.json
- Windows 기준: %APPDATA%\Claude\claude_desktop_config.json
- 이를 통해 Claude Desktop이 node /path/to/dis/server.js 명령어를 실행시킵니다.
- 주의! 그 전에 server.js가 빌드가 되어있어야 합니다!!
- 해당 빌드된 파일의 주소가 필요합니다!
- 서버가 표준 입력(STDIN)에서 JSON-RPC 메시지 수신
- 서버가 표준 출력(STDOUT)으로 응답 전송
위와 같은 동작으로 Claude 내에서 MCP Server를 실행할 수 있게 됩니다.
HttpStreamable Transport
반면 HttpStreamable 방식은 HTTP를 통한 스트리밍 통신을 지원하는 방식으로, 원격 서버 배포에 적합합니다.
🔹특징
- 원격 서버 지원: 네트워크를 통해 서버에 접근 가능
- 확장성: 여러 클라이언트가 동시에 연결 가능
- 배포 유연성: Docker 컨테이너나 클라우드 환경에 배포 가능.
MCP Server 배포에 대해서는 나중에 다시 살펴보겠습니다.
Register Tool
Business Logic
ContributorRecommender 클래스
ContributorRecommender 클래스는 Github 저장소의 커밋 히스토리를 분석하여 특정 파일, 디렉토리, 또는 PR에 대한 최적 리뷰어를 추천하는 비즈니스 로직입니다.
🔹 초기화 및 설정
constructor(inputs: ContributorRecommenderInputs) {
// GitHub API 클라이언트 생성
this.octokit = GitHubUtils.createGitHubAPIClient(inputs.githubToken);
// 저장소 정보 파싱
const { owner, repo } = GitHubUtils.parseRepoUrl(inputs.repoPath);
this.owner = owner;
this.repo = repo;
// 시간 범위 파싱 (유연한 형식 지원)
const timeRange = GitHubUtils.parseTimeRange(inputs.since, inputs.until);
this.since = timeRange.since; // ISO 형식으로 변환
this.until = timeRange.until;
}
🔹 주요 기능
- 저장소 경로 파싱
- 날짜 형식 지원
- 다국어 지원
🔹 분석 알고리즘
// 파일 목록에 대한 기여자 분석
private async analyzeFileContributors(files: string[]): Promise<ContributorCandidate[]> {
const contributors = new Map<string, { commits: number; files: Set<string> }>();
for (const file of files.slice(0, 10)) {
try {
const commits = await this.octokit.paginate(this.octokit.repos.listCommits, {
owner: this.owner,
repo: this.repo,
path: file,
since: this.since,
until: this.until,
per_page: 100,
});
for (const commit of commits) {
const commitData = commit as CommitData;
const author = commitData.author?.login;
if (!author) continue;
if (!contributors.has(author)) {
contributors.set(author, { commits: 0, files: new Set() });
}
const contributor = contributors.get(author)!;
contributor.commits++;
contributor.files.add(file);
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.warn(getI18n().t("errors.file_analysis", { file }), message);
}
}
return this.calculateContributorScores(contributors);
}
// 기여자 점수 계산 알고리즘
private calculateContributorScores(
contributors: Map<string, { commits: number; files: Set<string> }>,
sortFn?: (a: ContributorCandidate, b: ContributorCandidate) => number
): ContributorCandidate[] {
const totalCommits = Array.from(contributors.values()).reduce((sum, c) => sum + c.commits, 0);
const maxFiles = Math.max(...Array.from(contributors.values()).map((c) => c.files.size));
return Array.from(contributors.entries())
.map(([name, data]) => {
const ownership = maxFiles > 0 ? data.files.size / maxFiles : 0;
const commitScore = totalCommits > 0 ? data.commits / totalCommits : 0;
const score = commitScore * 0.6 + ownership * 0.4;
return {
name,
score: Number(score.toFixed(2)),
signals: {
ownership: Number(ownership.toFixed(2)),
recentCommits: data.commits,
recentReviews: 0,
},
};
})
.sort(sortFn || ContributorRecommender.defaultSortFn)
.slice(0, 10);
}
// 전략 패턴을 통한 분석 모드 선택
async analyze(): Promise<ContributorRecommendation> {
let candidates: ContributorCandidate[] = [];
const notes: string[] = [];
if (this.pr) {
candidates = await this.analyzePRContributors();
notes.push(getI18n().t("notes.pr_recommendation", { pr: this.pr }));
} else if (this.paths?.length) {
candidates = await this.analyzePathContributors();
notes.push(getI18n().t("notes.path_recommendation", { paths: this.paths.join(", ") }));
} else {
candidates = await this.analyzeBranchContributors();
notes.push(getI18n().t("notes.branch_recommendation", { branch: this.branch || "main" }));
}
const sinceDays = CommonUtils.getDaysDifference(this.since);
notes.push(getI18n().t("notes.analysis_period", { days: sinceDays }));
return {
candidates,
notes,
};
}
🔹 analyzeFileContributors
파일 목록을 받아 각 파일의 Git 히스토리를 분석해 기여자를 식별하는 메소드입니다.
- 파일별 커밋 히스토리 조회
- octokit을 활용하여 각 파일의 커밋 목록을 가져옵니다.
- 기여자 데이터 집계
- 각 커밋의 작성자를 추출해 Map에 저장합니다.
- Map의 키는 Github username, 값은 커밋수와 기여한 파일 Set입니다.
성능 최적화 전략
프로젝트의 크기가 커지면 커질수록 해당 작성자를 분석하는 것은 큰 시간이 들 수 있습니다. 따라서 저는 최대 10개 파일만 선분석하여 Github API 호출제한과 응답속도를 개선하였습니다.
🔹 calculateContributorScores
집계된 기여자 데이터를 점수화해 리뷰어 적합성을 평가하는 메소드입니다.
실제 알고리즘을 어떻게 작성할 지가 가장 고민이 되었던 부분입니다. 이 부분은 추가적으로 계속해서 개선이 되어야한다고 생각합니다.
현재 제가 작성한 알고리즘은 다음과 같습니다.
- 커밋 점수(60% 가중치)
- 커밋 점수 = (개인 커밋 수) / (전체 커밋 수)
- 이를 통해 기여 빈도를 나타낸다고 생각했습니다.
- 커밋 점수 = (개인 커밋 수) / (전체 커밋 수)
- 소유권 점수(40% 가중치)
- 소유권 점수 = (개인이 기여한 파일 수) / (최대 파일 수)
- 다양한 파일에 대한 기여도를 타나냅니다.
- 소유권 점수 = (개인이 기여한 파일 수) / (최대 파일 수)
- 최종 공식
- 최종 점수 = (커밋 점수 * 0.6 ) + (소유권 점수 * 0.4)
실제 커밋 점수에 더 높은 가중치를 둔 이유는 실제 코드 작성이 활발한 기여자가 더 파일에 대한 이해도가 높다고 생각하여 다음과 같은 점수를 책정하였습니다.
최종 점수를 통해 정렬 및 필터링을 진행하고, 상위 10명을 반환합니다.
🔹 analyze
메인 분석 기능으로서 입력 파라미터에 따라 적절한 분석 전략을 선택하여 분석할 수 있도록 하였습니다.(전략패턴 사용)
- PR 기반 분석
- PR의 변경된 파일 목록을 가져와 해당 파일들의 기여자 분석을 분석합니다.
- 경로 기반 분석
- 지정된 파일/디렉토리 경로의 커밋 히스토리를 분석해 해당 영역의 기여자를 추천합니다.
- 브랜치 기반 분석
- 특정 브랜치의 최근 커밋을 분석해 브랜치 활동에 기여한 개발자를 추천합니다.
이를 통해 나온 응답값은 다음과 같습니다.
{
candidates: [
{
name: "developer1",
score: 0.85,
signals: {
ownership: 0.9,
recentCommits: 25,
recentReviews: 0
}
},
// ... 상위 10명
],
notes: [
"PR #123 based recommendation",
"Analysis period: 90 days"
]
}
해당 분석 패턴을 통해 확장성, 유지보수성, 명확성을 챙길 수 있습니다.
Chart 표현
Githru는 데이터 시각화 분석 도구이므로, 분석 결과는 또한 차트로 표현하는 것이 핵심 기능으로 생각했습니다.
따라서 저는 ContributorRecommender Tool을 활용하여 HTML 기반 인터랙티브 차트를 생성하여 시각적으로 기여자 정보를 제공하도록 개발하였습니다.
// 1. HTML 템플릿 기반 차트 생성
generateChart(recommendation: ContributorRecommendation): string {
const { candidates, notes } = recommendation;
// 템플릿 파일 로드
let template = fs.readFileSync(
getHtmlAssets().path("contributors-chart.html"),
"utf8"
);
// 데이터 준비
const names = candidates.map(c => c.name);
const scores = candidates.map(c => c.score);
const commits = candidates.map(c => c.signals.recentCommits);
const ownership = candidates.map(c => c.signals.ownership);
// 템플릿 변수 치환
template = template.replace("{{NOTES}}", notesHtml);
template = template.replace("{{TABLE_ROWS}}", tableRowsHtml);
template = template.replace("{{CONTRIBUTORS}}", JSON.stringify(names));
template = template.replace("{{SCORES}}", JSON.stringify(scores));
template = template.replace("{{COMMITS}}", JSON.stringify(commits));
return template;
}
// 2. Chart.js를 활용한 인터렉티브 시각화
<!-- contributors-chart.html -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const contributors = {{CONTRIBUTORS}};
const scores = {{SCORES}};
const commits = {{COMMITS}};
const ctx = document.getElementById('contributorsChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: contributors,
datasets: [
{
label: 'Score',
data: scores,
backgroundColor: 'rgba(54, 162, 235, 0.8)',
yAxisID: 'y'
},
{
label: 'Commits',
data: commits,
backgroundColor: 'rgba(255, 99, 132, 0.8)',
yAxisID: 'y1'
}
]
},
options: {
scales: {
y: { beginAtZero: true, max: 1, title: { text: 'Score' } },
y1: { position: 'right', title: { text: 'Commits' } }
}
}
});
</script>
결과


차트 템플릿 표현

다음과 같이 저의 첫 Githru 및 MCP Server 도구를 개발완료하였습니다.
'OSSCA > 2025' 카테고리의 다른 글
| [OSSCA 2025] MCP Host (0) | 2025.11.16 |
|---|---|
| [OSSCA2025] MCP Client (0) | 2025.11.16 |
| [OSSCA2025] MCP Server (0) | 2025.10.26 |
| [OSSCA2025] MCP 개요 (0) | 2025.10.21 |
| [OSSCA2025] 2025 OSSCA 발대식 후기 (0) | 2025.07.14 |