λνν GitHub Pull Request μμ±μ μν Model Context Protocol (MCP) μλ²μ λλ€.
Git λ³κ²½μ¬ν λΆμ, PR λ΄μ© μλ μμ±, GitHub API μ°λ λ±μ κΈ°λ₯μ λ¨κ³λ³ μν¬νλ‘μ°λ‘ μ 곡ν©λλ€.
- λ¨κ³λ³ PR μμ± μν¬νλ‘μ°: λΈλμΉ μ ν β λ³κ²½μ¬ν λΆμ β PR λ΄μ© μμ± β GitHub PR μμ±
- λΉ λ₯Έ PR μμ±: ν λ²μ λͺ λ ΉμΌλ‘ μ 체 μν¬νλ‘μ° μ€ν (pr_smart)
- Git λ³κ²½μ¬ν λΆμ: μ»€λ° νμ€ν 리, λ³κ²½λ νμΌ, νμΌ νμ λ³ λΆλ₯
- μ§λ₯μ μΈ PR λ΄μ© μμ±: μ»€λ° λ©μμ§μ λ³κ²½μ¬νμ κΈ°λ°μΌλ‘ μλ μμ±
- λμ PR ν νλ¦Ώ μ§μ: μ μ₯μλ³ μ»€μ€ν ν νλ¦Ώ μλ κ°μ§
- JIRA ν°μΌ μ°λ: PR μ λͺ©κ³Ό λ³Έλ¬Έμ JIRA ν°μΌ λ²νΈ μλ ν¬ν¨
- GitHub API ν΅ν©: PR μμ±, λΈλμΉ push μλν
- μμ λλ ν 리 μλ κ°μ§: μ΄λ νλ‘μ νΈμμλ λ°λ‘ μ¬μ© κ°λ₯
- Java 21+
- Gradle 9.2+
- Git
- GitHub Personal Access Token (repo κΆν νμ)
- Claude Code λλ Claude Desktop
claude mcp add github-mcp -s user \
-e GITHUB_TOKEN=ghp_your_token_here \
-e PR_BASE_BRANCH=develop \
-e PR_JIRA_PREFIX=PROJ \
-- java -jar https://github.com/yunhalee05/github-mcp/releases/download/v1.0.0/github_mcp-0.0.1-SNAPSHOT.jarμ€μ νμΌ μμΉ: ~/.claude/settings.json
{
"mcpServers": {
"github-mcp": {
"command": "java",
"args": [
"-jar",
"/path/to/github-mcp/build/libs/github-mcp-1.0-SNAPSHOT.jar"
],
"env": {
"GITHUB_TOKEN": "ghp_your_token_here",
"PR_BASE_BRANCH": "develop",
"PR_JIRA_PREFIX": "PROJ",
"PR_TEMPLATE_PATH": "/path/to/custom/template.md"
}
}
}
}macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"github-mcp": {
"command": "java",
"args": [
"-jar",
"/path/to/github-mcp/build/libs/github-mcp-1.0-SNAPSHOT.jar"
],
"env": {
"GITHUB_TOKEN": "ghp_your_token_here",
"PR_BASE_BRANCH": "develop",
"PR_JIRA_PREFIX": "PROJ",
"PR_TEMPLATE_PATH": "/path/to/custom/template.md"
}
}
}
}| νκ²½λ³μ | νμ | κΈ°λ³Έκ° | μ€λͺ |
|---|---|---|---|
GITHUB_TOKEN |
β | - | GitHub Personal Access Token (repo κΆν νμ) |
PR_BASE_BRANCH |
β | develop | κΈ°λ³Έ Base λΈλμΉ |
PR_JIRA_PREFIX |
β | PROJ | JIRA ν°μΌ ν리ν½μ€ |
PR_TEMPLATE_PATH |
β | μλ κ°μ§ | 컀μ€ν PR ν νλ¦Ώ νμΌ κ²½λ‘ |
- μμ λλ ν 리λ μλμΌλ‘ κ°μ§λ©λλ€ - νκ²½λ³μ μ€μ λΆνμ
- Claude Code/Desktopμ΄ μ€νλλ λλ ν λ¦¬κ° μλμΌλ‘ μ¬μ©λ©λλ€
- AIκ° μ€ν 컨ν μ€νΈμμ μμ λλ ν 리λ₯Ό μλμΌλ‘ μ λ¬ν©λλ€
- https://github.com/settings/tokens μ μ
- "Generate new token (classic)" ν΄λ¦
- νμν κΆν μ ν:
repo(μ 체)workflow(μ νμ¬ν)
- μμ±λ ν ν°μ 볡μ¬νμ¬ μ€μ νμΌμ μΆκ°
# νλ‘μ νΈ ν΄λ‘
git clone https://github.com/YOUR_USERNAME/github-mcp.git
cd github-mcp
# λΉλ
./gradlew clean build
# JAR νμΌ νμΈ
ls -la build/libs/github-mcp-1.0-SNAPSHOT.jarsrc/main/kotlin/com/yunhalee/github_mcp/
βββ McpServer.kt # MCP μλ² λ©μΈ μνΈλ¦¬ν¬μΈνΈ
βββ component/
β βββ TemplateLoader.kt # PR ν
νλ¦Ώ λ‘λ
βββ service/
β βββ GitService.kt # Git λͺ
λ Ήμ΄ μ€ν μλΉμ€ (μ±κΈν€)
β βββ GitHubService.kt # GitHub API νΈμΆ μλΉμ€
βββ tool/
βββ ToolContext.kt # Tool 곡μ 컨ν
μ€νΈ
βββ ToolRegistry.kt # Tool λ±λ‘ κ΄λ¦¬μ
βββ StartPrWorkflowTool.kt # PR μν¬νλ‘μ° μμ
βββ SelectBaseBranchTool.kt # Base λΈλμΉ μ ν λ° λΆμ
βββ GeneratePrContentTool.kt # PR λ΄μ© μμ±
βββ CreatePrConfirmedTool.kt # PR μμ± μ€ν
βββ GetCurrentBranchTool.kt # νμ¬ λΈλμΉ νμΈ
βββ PrSmartTool.kt # λΉ λ₯Έ PR μμ± (μν¬νλ‘μ° ν΅ν©)
Kotlin MCP SDKμ 곡μ ν¨ν΄μ μ¬μ©νμ¬ Toolμ μ μν©λλ€:
fun createStartPrWorkflowTool(context: ToolContext) = RegisteredTool(
Tool(
name = "start_pr_workflow",
description = "PR μμ± μν¬νλ‘μ°λ₯Ό μμν©λλ€...",
inputSchema = ToolSchema(
properties = buildJsonObject {
put("working_dir", buildJsonObject {
put("type", "string")
put("description", "νμ¬ μμ
λλ ν 리 κ²½λ‘ - REQUIRED")
})
},
required = listOf("working_dir")
)
)
) { request ->
val workingDir = request.arguments?.get("working_dir")?.jsonPrimitive?.content
?: return@RegisteredTool CallToolResult(
content = listOf(TextContent(text = "β working_dirμ΄ νμν©λλ€.")),
isError = true
)
// Tool λ‘μ§ κ΅¬ν
CallToolResult(content = listOf(TextContent(text = result)))
}λͺ¨λ Toolμ΄ κ³΅μ νλ 컨ν μ€νΈ:
data class ToolContext(
val defaultBaseBranch: String,
val jiraPrefix: String,
val githubService: GitHubService?,
val prTemplatePath: String? = null
) {
val gitService = GitService() // μ±κΈν€ μΈμ€ν΄μ€
private val templateLoader: TemplateLoader by lazy {
TemplateLoader(customTemplatePath = prTemplatePath)
}
fun loadPrTemplate(workingDir: String): String {
return templateLoader.loadPrTemplate(workingDir)
}
}μ£Όμ λ³κ²½μ¬ν:
defaultWorkingDirμ κ±° - λ μ΄μ νμνμ§ μμgitServiceμ±κΈν€ μΈμ€ν΄μ€ μΆκ°templateLoaderμ§μ° μ΄κΈ°νλ‘ PR ν νλ¦Ώ λ‘λ©
λͺ¨λ Git μμ
μ μ²λ¦¬νλ©°, workingDirμ λ©μλ νλΌλ―Έν°λ‘ λ°μ΅λλ€:
class GitService {
suspend fun getCurrentBranch(workingDir: String): Result<String>
suspend fun getBranches(workingDir: String): Result<List<String>>
suspend fun getDiff(workingDir: String, baseBranch: String, currentBranch: String): Result<String>
suspend fun getChangedFiles(workingDir: String, baseBranch: String, currentBranch: String): Result<List<String>>
suspend fun getCommits(workingDir: String, baseBranch: String, currentBranch: String): Result<List<String>>
suspend fun getCommitCount(workingDir: String, baseBranch: String, currentBranch: String): Result<Int>
suspend fun pushBranch(workingDir: String, branch: String): Result<String>
suspend fun fetchBranch(workingDir: String, branch: String): Result<String>
suspend fun checkRemoteBranchExists(workingDir: String, branch: String): Result<Boolean>
suspend fun getRepositoryInfo(workingDir: String): Result<Map<String, String>>
}μ€κ³ νΉμ§:
- μ±κΈν€ ν¨ν΄μΌλ‘ μΈμ€ν΄μ€ μ¬μ¬μ©
workingDirμ μΈμ€ν΄μ€ λ³μκ° μλ λ©μλ νλΌλ―Έν°λ‘ λ°μ- MCP μλ²κ° μ¬λ¬ νλ‘μ νΈλ₯Ό λμμ μ²λ¦¬ κ°λ₯
PR ν νλ¦Ώμ λμ μΌλ‘ λ‘λν©λλ€:
class TemplateLoader(private val customTemplatePath: String? = null) {
fun loadPrTemplate(workingDir: String): String {
val templatePaths = listOf(
"$workingDir/.github/PULL_REQUEST_TEMPLATE.md",
"$workingDir/.github/pull_request_template.md",
"$workingDir/docs/pull_request_template.md",
customTemplatePath
)
for (path in templatePaths) {
path?.let {
val file = File(it)
if (file.exists() && file.isFile) {
return file.readText()
}
}
}
return DEFAULT_PR_TEMPLATE
}
}Tool λ±λ‘μ μ€μ κ΄λ¦¬:
class ToolRegistry(private val context: ToolContext) {
fun getAllTools(): List<RegisteredTool> = listOf(
createStartPrWorkflowTool(context),
createSelectBaseBranchTool(context),
createGeneratePrContentTool(context),
createCreatePrConfirmedTool(context),
createGetCurrentBranchTool(context),
createPrSmartTool(context)
)
fun registerAll(server: Server) {
getAllTools().forEach { tool ->
server.addTool(tool.tool, tool.handler)
}
}
}PR μμ± μν¬νλ‘μ°λ₯Ό μμν©λλ€.
Parameters:
working_dir(νμ): μμ λλ ν 리 κ²½λ‘ (AIκ° μλ μ λ¬)
λμ:
- νμ¬ Git λΈλμΉ νμΈ
- main/master λΈλμΉ μ²΄ν¬
- μ¬μ© κ°λ₯ν base λΈλμΉ λͺ©λ‘ λ°ν
λ€μ λ¨κ³: μ¬μ©μκ° base λΈλμΉλ₯Ό μ ννλ©΄ select_base_branch νΈμΆ
Base λΈλμΉλ₯Ό μ ννκ³ λ³κ²½μ¬νμ λΆμν©λλ€.
Parameters:
base_branch(νμ): Base λΈλμΉ μ΄λ¦working_dir(νμ): μμ λλ ν 리 κ²½λ‘ (AIκ° μλ μ λ¬)
λμ:
- λΈλμΉ μ‘΄μ¬ νμΈ λ° fetch
- λ³κ²½λ νμΌ λͺ©λ‘ μ‘°ν
- μ»€λ° νμ€ν 리 λΆμ
- νμΌ νμ λ³ λΆλ₯
λ€μ λ¨κ³: μ¬μ©μκ° JIRA ν°μΌμ μ
λ ₯νλ©΄ generate_pr_content νΈμΆ
JIRA ν°μΌκ³Ό λ³κ²½μ¬νμ κΈ°λ°μΌλ‘ PR μ λͺ©κ³Ό λ³Έλ¬Έμ μμ±ν©λλ€.
Parameters:
base_branch(νμ): Base λΈλμΉjira_ticket(νμ): JIRA ν°μΌ λ²νΈ (μμΌλ©΄ "μμ")additional_context(μ ν): μΆκ° 컨ν μ€νΈworking_dir(νμ): μμ λλ ν 리 κ²½λ‘ (AIκ° μλ μ λ¬)
λμ:
- Git diff λΆμ
- PR μ λͺ© μμ± (JIRA ν°μΌ ν¬ν¨)
- μ μ₯μμ PR ν νλ¦Ώ μλ κ°μ§ λ° λ‘λ
- AIμκ² λ³κ²½μ¬ν μμ½ λ° ν νλ¦Ώ μμ± μμ²
ν νλ¦Ώ μ°μ μμ:
.github/PULL_REQUEST_TEMPLATE.md.github/pull_request_template.mddocs/pull_request_template.mdPR_TEMPLATE_PATHνκ²½λ³μ κ²½λ‘- κΈ°λ³Έ λ΄μ₯ ν νλ¦Ώ
λ€μ λ¨κ³: μ¬μ©μκ° νμΈνλ©΄ create_pr_confirmed νΈμΆ
μ€μ λ‘ GitHub PRμ μμ±ν©λλ€.
Parameters:
title(νμ): PR μ λͺ©body(νμ): PR λ³Έλ¬Έbase_branch(νμ): Base λΈλμΉworking_dir(νμ): μμ λλ ν 리 κ²½λ‘ (AIκ° μλ μ λ¬)
λμ:
- λΈλμΉ push (μ격μ μλ κ²½μ°)
- Repository μ 보 μ‘°ν (owner/repo μΆμΆ)
- GitHub APIλ‘ PR μμ±
- μμ±λ PR URL λ°ν
νμ¬ Git λΈλμΉλ₯Ό νμΈν©λλ€.
Parameters:
working_dir(νμ): μμ λλ ν 리 κ²½λ‘ (AIκ° μλ μ λ¬)
λμ:
- νμ¬ λΈλμΉλͺ λ°ν
μ 체 μν¬νλ‘μ°λ₯Ό ν λ²μ μ€νν©λλ€.
Parameters:
base_branch(νμ): Base λΈλμΉjira_ticket(μ ν): JIRA ν°μΌ λ²νΈadditional_context(μ ν): μΆκ° 컨ν μ€νΈworking_dir(νμ): μμ λλ ν 리 κ²½λ‘ (AIκ° μλ μ λ¬)
λμ:
- λΈλμΉ νμΈ β λ³κ²½μ¬ν λΆμ β PR λ΄μ© μμ±μ ν λ²μ μ²λ¦¬
- μμ±λ PR λ΄μ©μ μ¬μ©μμκ² λ³΄μ¬μ£Όκ³ νμΈ μμ²
- νμΈ μ
create_pr_confirmedνΈμΆ
src/main/kotlin/com/yunhalee/github_mcp/tool/YourNewTool.kt:
src/main/kotlin/com/yunhalee/github_mcp/tool/ToolRegistry.kt:
fun getAllTools(): List<RegisteredTool> = listOf(
createStartPrWorkflowTool(context),
createSelectBaseBranchTool(context),
// ...
createYourNewTool(context) // μΆκ°!
)./gradlew clean build
java -jar build/libs/github-mcp-1.0-SNAPSHOT.jarμ€μ: GitServiceλ μ±κΈν€μΌλ‘ μ¬μ©λλ©°, workingDirμ λ©μλ νλΌλ―Έν°λ‘ μ λ¬ν©λλ€.
// ToolContextλ₯Ό ν΅ν΄ ν
νλ¦Ώ λ‘λ
val template = context.loadPrTemplate(workingDir)
// ν
νλ¦Ώμ μ€ν μ μ₯μμ .github/PULL_REQUEST_TEMPLATE.md λλ
// PR_TEMPLATE_PATH νκ²½λ³μ κ²½λ‘μμ μλμΌλ‘ λ‘λλ©λλ€μ΄ MCP μλ²λ μμ λλ ν 리λ₯Ό μλμΌλ‘ κ°μ§ν©λλ€:
- Claude Code/Desktopμ΄ μ€νλ λ νμ¬ μμ λλ ν 리λ₯Ό AI μ€ν 컨ν μ€νΈλ‘ μ λ¬
- AIκ°
<env>Working directory: /path/to/project</env>μ 보λ₯Ό μ½μ - κ° Tool νΈμΆ μ AIκ° μλμΌλ‘
working_dirνλΌλ―Έν°μ κ²½λ‘λ₯Ό μ λ¬ - MCP μλ²κ° ν΄λΉ λλ ν 리μμ Git λͺ λ Ήμ΄ μ€ν
μ₯μ :
- νλ‘μ νΈλ§λ€ λ³λ μ€μ λΆνμ
- Claude Code/Desktopμ μ΄λ νλ‘μ νΈμμ μ€ννλ μλμΌλ‘ ν΄λΉ νλ‘μ νΈμ Git μ μ₯μ μΈμ
- μ€μ νμΌ κ°μν - νκ²½λ³μλ‘ WORKING_DIR μ§μ λΆνμ
- μ¬λ¬ νλ‘μ νΈμμ λμμ μμ κ°λ₯
MCP μλ²λ STDIO(Standard Input/Output)λ‘ ν΅μ ν©λλ€:
- HTTP μλ² λΆνμ
- Spring Boot Context λΆνμ
- Dependency Injection λΆνμ
- μμ Kotlin + MCP SDKλ‘ μΆ©λΆ
κ²°κ³Ό:
- 13MBμ κ°λ²Όμ΄ JAR (Spring Boot μ¬μ© μ 30-40MB)
- λΉ λ₯Έ μμ μκ°
- λͺ νν μμ‘΄μ±
dependencies {
// MCP SDK
implementation("io.modelcontextprotocol:kotlin-sdk:0.8.0")
// Ktor Client (HTTP μμ²)
implementation("io.ktor:ktor-client-cio:3.3.2")
implementation("io.ktor:ktor-client-content-negotiation:3.3.2")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.3.2")
// Kotlin IO (STDIO ν΅μ )
implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.5.4")
// Logging
implementation("org.slf4j:slf4j-simple:2.0.16")
}- Model Context Protocol
- MCP Kotlin SDK
- MCP Kotlin SDK Documentation
- Claude Code λ¬Έμ
- Building an MCP Server in Kotlin
μμΈν μ€μΉ λ° μ¬μ© λ°©λ²μ QUICKSTART.mdλ₯Ό μ°Έκ³ νμΈμ.
μ΄ νλ‘μ νΈλ κ°μΈ νλ‘μ νΈμ λλ€.
λ²κ·Έ 리ν¬νΈλ κΈ°λ₯ μ μμ μ΄μλ‘ λ±λ‘ν΄μ£ΌμΈμ.