Skip to content

Commit f6bf9cf

Browse files
authored
feat: add jacoco reports (#13)
1 parent 7505e77 commit f6bf9cf

6 files changed

Lines changed: 166 additions & 105 deletions

File tree

README.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Model Context Protocol Code Analysis Server
1+
# 🤖 Model Context Protocol Code Analysis Server
22

33
A Kotlin server application that analyzes GitHub repositories using AI models through the Model Context Protocol (MCP).
44

@@ -8,7 +8,6 @@ A Kotlin server application that analyzes GitHub repositories using AI models th
88
- Extract code structure and relationships
99
- Process code using Model Context Protocol
1010
- Generate detailed insights and summaries
11-
- Functional architecture with immutable data classes
1211
- Multiple server modes (stdio, SSE)
1312

1413
## Getting Started
@@ -17,7 +16,7 @@ A Kotlin server application that analyzes GitHub repositories using AI models th
1716

1817
- JDK 23 or higher
1918
- Kotlin 1.9.x
20-
- Gradle 8.0 or higher
19+
- Gradle 8.14 or higher
2120
- [Ollama](https://github.com/ollama/ollama) 3.2 or higher (for model API)
2221
- [MCP Inspector](https://github.com/modelcontextprotocol/inspector) (for model context protocol)
2322

@@ -42,6 +41,27 @@ A Kotlin server application that analyzes GitHub repositories using AI models th
4241
npx @modelcontextprotocol/inspector
4342
```
4443

44+
5. You can access the MCP Inspector at `http://127.0.0.1:6274/` and configure the `Arguments` to start the server:
45+
46+
![Connect](https://raw.githubusercontent.com/eschizo/mcp-github-code-analyzer/main/img/mcp_connect_server.png)
47+
48+
Use the following arguments:
49+
50+
```bash
51+
~/mcp-github-code-analyzer/build/libs/mcp-github-code-analyzer-0.1.0-SNAPSHOT.jar --stdio
52+
```
53+
54+
6. Click `Connect` to start the MCP Server.
55+
56+
7. Then you can click the tab `Tools` to discover the available tools. The Tool `analyze-repository` should be listed
57+
and ready to be used. Click on the `analyze-repository` tool to see its details and parameters:
58+
59+
![Tools Tab](https://raw.githubusercontent.com/eschizo/mcp-github-code-analyzer/main/img/mcp_tools_tab.png)
60+
61+
8. Finally, capture the `repoUrl` and `branch` parameters and click `Run Tool` to start the analysis:
62+
63+
![Run Tool](https://raw.githubusercontent.com/eschizo/mcp-github-code-analyzer/main/img/mcp_inspector_run_tool.png)
64+
4565
### Configuration
4666

4767
The application uses environment variables for configuration:
@@ -82,11 +102,11 @@ This server implements the Model Context Protocol (MCP) and provides the followi
82102

83103
Required parameters:
84104

85-
- repoUrl: GitHub repository URL (e.g., https://github.com/owner/repo)
105+
- `repoUrl`: GitHub repository URL (e.g., https://github.com/owner/repo)
86106

87107
Optional parameters:
88108

89-
- branch: Branch to analyze (default: main)
109+
- `branch`: Branch to analyze (default: main)
90110

91111
## Project Structure
92112

79.6 KB
Loading

img/mcp_inspector_run_tool.png

515 KB
Loading

img/mcp_inspector_tools_tab.png

117 KB
Loading

src/main/kotlin/mcp/code/analysis/service/CodeAnalyzer.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,22 @@ data class CodeAnalyzer(
3636
.map { file ->
3737
val relativePath = file.absolutePath.substring(repoDir.absolutePath.length + 1)
3838
val lang = getLanguageFromExtension(file.extension)
39-
val safeContent = file.readLines().joinToString("\n")
40-
"---\nFile: $relativePath\n~~~$lang\n$safeContent\n~~~"
39+
val content = file.readLines().joinToString("\n")
40+
"""---
41+
|File: $relativePath
42+
|~~~$lang
43+
|$content
44+
|~~~"""
45+
.trimIndent()
4146
}
4247
.toList()
4348
.also { snippets ->
4449
logger.info("Collected ${snippets.size} code snippets from ${repoDir.absolutePath}")
45-
logger.debug("Snippets Found:\n${snippets.joinToString("\n")}")
50+
logger.debug(
51+
"""Snippets Found:
52+
|${snippets.joinToString("\n")}"""
53+
.trimIndent()
54+
)
4655
}
4756

4857
/**

src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt

Lines changed: 129 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.mockk.every
55
import io.mockk.mockk
66
import io.mockk.verify
77
import java.io.File
8+
import java.lang.Exception
89
import kotlinx.coroutines.runBlocking
910
import kotlinx.coroutines.test.runTest
1011
import org.junit.jupiter.api.BeforeEach
@@ -41,7 +42,15 @@ class RepositoryAnalysisServiceTest {
4142
val branch = "main"
4243
val clonedRepo = File(tempDir, "repo")
4344
val readmeContent = "# Test Repository"
44-
val codeSnippets = listOf("---\nFile: src/Main.kt\n~~~kotlin\nfun main() {}\n~~~")
45+
val codeSnippets =
46+
listOf(
47+
"""---
48+
|File: src/Main.kt
49+
|~~~kotlin
50+
|fun main() {}
51+
|~~~"""
52+
.trimIndent()
53+
)
4554
val modelResponse = "Repository Analysis: This is a simple Kotlin project"
4655
val insightsPrompt = "insights prompt"
4756
val summaryPrompt = "summary prompt"
@@ -63,113 +72,136 @@ class RepositoryAnalysisServiceTest {
6372
verify { codeAnalyzer.findReadmeFile(clonedRepo) }
6473
verify { codeAnalyzer.collectAllCodeSnippets(clonedRepo) }
6574
verify { runBlocking { modelContextService.generateResponse(any()) } }
75+
}
6676

67-
@Test
68-
fun `analyzeRepository should handle git errors`() = runTest {
69-
// Arrange
70-
val repoUrl = "https://github.com/user/repo"
71-
val branch = "main"
77+
@Test
78+
fun `analyzeRepository should handle git errors`() = runTest {
79+
// Arrange
80+
val repoUrl = "https://github.com/user/repo"
81+
val branch = "main"
7282

73-
every { gitService.cloneRepository(repoUrl, branch) } throws RuntimeException("Git error")
83+
every { gitService.cloneRepository(repoUrl, branch) } throws Exception("Error cloning repository")
7484

75-
// Act & Assert
76-
val exception = assertThrows<RuntimeException> { repositoryAnalysisService.analyzeRepository(repoUrl, branch) }
77-
assert(exception.message?.contains("Error cloning repository") == true)
78-
}
85+
// Act & Assert
86+
val exception = assertThrows<Exception> { repositoryAnalysisService.analyzeRepository(repoUrl, branch) }
87+
assert(exception.message?.contains("Error cloning repository") == true)
88+
}
7989

80-
@Test
81-
fun `analyzeRepository should handle code analysis errors`() {
82-
// Arrange
83-
val repoUrl = "https://github.com/user/repo"
84-
val branch = "main"
85-
val clonedRepo = File(tempDir, "repo")
90+
@Test
91+
fun `analyzeRepository should handle code analysis errors`() {
92+
// Arrange
93+
val repoUrl = "https://github.com/user/repo"
94+
val branch = "main"
95+
val clonedRepo = File(tempDir, "repo")
8696

87-
every { gitService.cloneRepository(repoUrl, branch) } returns clonedRepo
88-
every { codeAnalyzer.findReadmeFile(clonedRepo) } throws RuntimeException("Analysis error")
97+
every { gitService.cloneRepository(repoUrl, branch) } returns clonedRepo
98+
every { codeAnalyzer.findReadmeFile(clonedRepo) } throws Exception("Error analyzing repository code")
8999

90-
// Act & Assert
91-
val exception =
92-
assertThrows<RuntimeException> { runBlocking { repositoryAnalysisService.analyzeRepository(repoUrl, branch) } }
100+
// Act & Assert
101+
val exception =
102+
assertThrows<Exception> { runBlocking { repositoryAnalysisService.analyzeRepository(repoUrl, branch) } }
93103

94-
assert(exception.message?.contains("Error analyzing repository code") == true)
95-
}
104+
assert(exception.message?.contains("Error analyzing repository code") == true)
105+
}
96106

97-
@Test
98-
fun `analyzeRepository should handle model service errors`() {
99-
// Arrange
100-
val repoUrl = "https://github.com/user/repo"
101-
val branch = "main"
102-
val clonedRepo = File(tempDir, "repo")
103-
val readmeContent = "# Test Repository"
104-
val codeSnippets = listOf("---\nFile: src/Main.kt\n~~~kotlin\nfun main() {}\n~~~")
105-
106-
every { gitService.cloneRepository(repoUrl, branch) } returns clonedRepo
107-
every { codeAnalyzer.findReadmeFile(clonedRepo) } returns readmeContent
108-
every { codeAnalyzer.collectAllCodeSnippets(clonedRepo) } returns codeSnippets
109-
coEvery { modelContextService.generateResponse(any()) } throws RuntimeException("Model error")
110-
111-
// Act & Assert
112-
val exception =
113-
assertThrows<RuntimeException> { runBlocking { repositoryAnalysisService.analyzeRepository(repoUrl, branch) } }
114-
115-
assert(exception.message?.contains("Error generating repository analysis") == true)
116-
}
107+
@Test
108+
fun `analyzeRepository should handle model service errors`() {
109+
// Arrange
110+
val repoUrl = "https://github.com/user/repo"
111+
val branch = "main"
112+
val clonedRepo = File(tempDir, "repo")
113+
val readmeContent = "# Test Repository"
114+
val codeSnippets =
115+
listOf(
116+
"""---
117+
|File: src/Main.kt
118+
|~~~kotlin
119+
|fun main() {}
120+
|~~~"""
121+
.trimIndent()
122+
)
117123

118-
@Test
119-
fun `analyzeRepository should handle empty code snippets`() = runTest {
120-
// Arrange
121-
val repoUrl = "https://github.com/user/repo"
122-
val branch = "main"
123-
val clonedRepo = File(tempDir, "repo")
124-
val readmeContent = "# Test Repository"
125-
val emptySnippets = emptyList<String>()
126-
val modelResponse = "Repository Analysis: No code files found"
127-
128-
every { gitService.cloneRepository(repoUrl, branch) } returns clonedRepo
129-
every { codeAnalyzer.findReadmeFile(clonedRepo) } returns readmeContent
130-
every { codeAnalyzer.collectAllCodeSnippets(clonedRepo) } returns emptySnippets
131-
coEvery { modelContextService.generateResponse(any()) } returns modelResponse
132-
133-
// Act
134-
val result = repositoryAnalysisService.analyzeRepository(repoUrl, branch)
135-
136-
// Assert
137-
assert(result == modelResponse)
138-
verify { codeAnalyzer.collectAllCodeSnippets(clonedRepo) }
139-
verify {
140-
runBlocking {
141-
modelContextService.generateResponse(
142-
match { prompt -> prompt.contains("No code files found") || prompt.contains("0 code snippets") }
143-
)
144-
}
145-
}
124+
every { gitService.cloneRepository(repoUrl, branch) } returns clonedRepo
125+
every { codeAnalyzer.findReadmeFile(clonedRepo) } returns readmeContent
126+
every { codeAnalyzer.collectAllCodeSnippets(clonedRepo) } returns codeSnippets
127+
coEvery { modelContextService.generateResponse(any()) } throws Exception("Error analyzing repository")
128+
129+
// Act & Assert
130+
val exception =
131+
assertThrows<Exception> { runBlocking { repositoryAnalysisService.analyzeRepository(repoUrl, branch) } }
132+
133+
assert(exception.message?.contains("Error analyzing repository") == true)
134+
}
135+
136+
@Test
137+
fun `analyzeRepository should handle empty code snippets`() = runTest {
138+
// Arrange
139+
val repoUrl = "https://github.com/user/repo"
140+
val branch = "main"
141+
val clonedRepo = File(tempDir, "repo")
142+
val readmeContent = "# Test Repository"
143+
val emptySnippets = emptyList<String>()
144+
val insightsPrompt = "Insights prompt"
145+
val summaryPrompt = "Summary prompt"
146+
val modelResponse = "Repository Analysis"
147+
148+
every { gitService.cloneRepository(repoUrl, branch) } returns clonedRepo
149+
every { codeAnalyzer.findReadmeFile(clonedRepo) } returns readmeContent
150+
every { codeAnalyzer.analyzeStructure(clonedRepo) } returns emptyMap()
151+
every { codeAnalyzer.collectAllCodeSnippets(clonedRepo) } returns emptySnippets
152+
every { modelContextService.buildInsightsPrompt(readmeContent) } returns insightsPrompt
153+
every { modelContextService.buildSummaryPrompt(any(), any(), any()) } returns summaryPrompt
154+
coEvery { modelContextService.generateResponse(any()) } returns modelResponse
155+
156+
// Act
157+
val result = repositoryAnalysisService.analyzeRepository(repoUrl, branch)
158+
159+
// Assert
160+
assert(result == modelResponse)
161+
verify { codeAnalyzer.collectAllCodeSnippets(clonedRepo) }
162+
verify {
163+
runBlocking { modelContextService.generateResponse("Insights prompt") }
164+
runBlocking { modelContextService.generateResponse("Summary prompt") }
146165
}
166+
}
167+
168+
@Test
169+
fun `analyzeRepository should handle missing README`() = runTest {
170+
// Arrange
171+
val repoUrl = "https://github.com/user/repo"
172+
val branch = "main"
173+
val clonedRepo = File(tempDir, "repo")
174+
val noReadme = "No README content available."
175+
val codeSnippets =
176+
listOf(
177+
""""---
178+
|File: src/Main.kt
179+
|~~~kotlin
180+
|fun main() {}
181+
|~~~"""
182+
.trimIndent()
183+
)
184+
val modelResponse = "Repository Analysis: Kotlin project without README"
185+
val insightsPrompt = "No README content available"
186+
val summaryPrompt = "Summary prompt"
187+
188+
every { gitService.cloneRepository(repoUrl, branch) } returns clonedRepo
189+
every { codeAnalyzer.findReadmeFile(clonedRepo) } returns noReadme
190+
every { codeAnalyzer.analyzeStructure(clonedRepo) } returns emptyMap()
191+
every { codeAnalyzer.collectAllCodeSnippets(clonedRepo) } returns codeSnippets
192+
every { modelContextService.buildInsightsPrompt(noReadme) } returns insightsPrompt
193+
every { modelContextService.buildSummaryPrompt(any(), any(), any()) } returns summaryPrompt
194+
coEvery { modelContextService.generateResponse(any()) } returns modelResponse
147195

148-
@Test
149-
fun `analyzeRepository should handle missing README`() = runTest {
150-
// Arrange
151-
val repoUrl = "https://github.com/user/repo"
152-
val branch = "main"
153-
val clonedRepo = File(tempDir, "repo")
154-
val noReadme = "No README content available."
155-
val codeSnippets = listOf("---\nFile: src/Main.kt\n~~~kotlin\nfun main() {}\n~~~")
156-
val modelResponse = "Repository Analysis: Kotlin project without README"
157-
158-
every { gitService.cloneRepository(repoUrl, branch) } returns clonedRepo
159-
every { codeAnalyzer.findReadmeFile(clonedRepo) } returns noReadme
160-
every { codeAnalyzer.collectAllCodeSnippets(clonedRepo) } returns codeSnippets
161-
coEvery { modelContextService.generateResponse(any()) } returns modelResponse
162-
163-
// Act
164-
val result = repositoryAnalysisService.analyzeRepository(repoUrl, branch)
165-
166-
// Assert
167-
assert(result == modelResponse)
168-
verify { codeAnalyzer.findReadmeFile(clonedRepo) }
169-
verify {
170-
runBlocking {
171-
modelContextService.generateResponse(match { prompt -> prompt.contains("No README content available") })
172-
}
196+
// Act
197+
val result = repositoryAnalysisService.analyzeRepository(repoUrl, branch)
198+
199+
// Assert
200+
assert(result == modelResponse)
201+
verify { codeAnalyzer.findReadmeFile(clonedRepo) }
202+
verify {
203+
runBlocking {
204+
modelContextService.generateResponse(match { prompt -> prompt.contains("No README content available") })
173205
}
174206
}
175207
}

0 commit comments

Comments
 (0)