Skip to content

Commit fd3a467

Browse files
authored
feat: enable llama chat (#16)
1 parent 981b7f8 commit fd3a467

File tree

8 files changed

+284
-269
lines changed

8 files changed

+284
-269
lines changed

build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ version = rootProject.scmVersion.version
2626
description = "MCP Server for GitHub Code Repositories Analysis"
2727

2828
dependencies {
29-
val ktorVersion = "3.1.3"
29+
val ktorVersion = "3.0.2"
3030
val coroutinesVersion = "1.10.2"
3131

3232
// Kotlin standard library
@@ -48,6 +48,7 @@ dependencies {
4848
// Logging
4949
implementation("ch.qos.logback:logback-classic:1.5.18")
5050
implementation("org.slf4j:jul-to-slf4j:2.0.17")
51+
// implementation("org.slf4j:slf4j-nop:2.0.9")
5152

5253
// Coroutines
5354
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

src/main/kotlin/mcp/code/analysis/server/Server.kt

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ import io.ktor.server.engine.*
77
import io.ktor.server.response.*
88
import io.ktor.server.routing.*
99
import io.ktor.server.sse.*
10+
import io.ktor.sse.ServerSentEvent
1011
import io.ktor.util.collections.*
1112
import io.modelcontextprotocol.kotlin.sdk.*
1213
import io.modelcontextprotocol.kotlin.sdk.server.*
1314
import io.modelcontextprotocol.kotlin.sdk.server.Server as SdkServer
15+
import kotlin.text.get
1416
import kotlinx.coroutines.Job
17+
import kotlinx.coroutines.delay
18+
import kotlinx.coroutines.launch
1519
import kotlinx.coroutines.runBlocking
1620
import kotlinx.io.asSink
1721
import kotlinx.io.asSource
@@ -73,13 +77,18 @@ open class Server(
7377

7478
embeddedServer(CIO, host = "0.0.0.0", port = port) {
7579
install(SSE)
80+
7681
routing {
7782
sse("/sse") {
78-
val transport = SseServerTransport("/message", this)
79-
val server: SdkServer = configureServer()
83+
launch {
84+
while (true) {
85+
send(ServerSentEvent(event = "heartbeat"))
86+
delay(15_000)
87+
}
88+
}
8089

81-
// For SSE, you can also add prompts/tools/resources if needed:
82-
// server.addTool(...), server.addPrompt(...), server.addResource(...)
90+
val transport = SseServerTransport("/message", this)
91+
val server = configureServer()
8392

8493
servers[transport.sessionId] = server
8594

@@ -90,16 +99,28 @@ open class Server(
9099

91100
server.connect(transport)
92101
}
102+
93103
post("/message") {
94-
logger.info("Received Message")
95-
val sessionId: String = call.request.queryParameters["sessionId"]!!
96-
val transport = servers[sessionId]?.transport as? SseServerTransport
97-
if (transport == null) {
98-
call.respond(HttpStatusCode.NotFound, "Session not found")
99-
return@post
100-
}
104+
try {
105+
106+
val sessionId =
107+
call.request.queryParameters["sessionId"]
108+
?: return@post call.respond(HttpStatusCode.BadRequest, "Missing sessionId parameter")
101109

102-
transport.handlePostMessage(call)
110+
val transport = servers[sessionId]?.transport as? SseServerTransport
111+
if (transport == null) {
112+
call.respond(HttpStatusCode.NotFound, "Session not found")
113+
return@post
114+
}
115+
116+
logger.debug("Handling message for session: $sessionId")
117+
transport.handlePostMessage(call)
118+
119+
call.respond(HttpStatusCode.OK)
120+
} catch (e: Exception) {
121+
logger.error("Error handling message: ${e.message}", e)
122+
call.respond(HttpStatusCode.InternalServerError, "Error handling message: ${e.message}")
123+
}
103124
}
104125
}
105126
}
@@ -112,10 +133,15 @@ open class Server(
112133
* @param port The port number on which the SSE MCP server will listen for client connections.
113134
*/
114135
open fun runSseMcpServerUsingKtorPlugin(port: Int): Unit = runBlocking {
115-
logger.info("Starting SSE server on port $port")
116-
logger.info("Use inspector to connect to http://localhost:$port/sse")
136+
logger.debug("Starting SSE server on port $port")
137+
logger.debug("Use inspector to connect to http://localhost:$port/sse")
117138

118-
embeddedServer(CIO, host = "0.0.0.0", port = port) { mcp { configureServer() } }.start(wait = true)
139+
embeddedServer(CIO, host = "0.0.0.0", port = port) {
140+
mcp {
141+
return@mcp configureServer()
142+
}
143+
}
144+
.start(wait = true)
119145
}
120146

121147
/**

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ data class CodeAnalyzer(
3737
val relativePath = file.absolutePath.substring(repoDir.absolutePath.length + 1)
3838
val lang = getLanguageFromExtension(file.extension)
3939
val content = file.readLines().joinToString("\n")
40-
"""|---
41-
|File: $relativePath
40+
"""|--- File: $relativePath
4241
|~~~$lang
4342
|$content
4443
|~~~"""

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

Lines changed: 86 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,11 @@ import mcp.code.analysis.config.AppConfig
1616
import org.slf4j.Logger
1717
import org.slf4j.LoggerFactory
1818

19-
@Serializable
20-
data class OllamaRequest(
21-
val model: String,
22-
val prompt: String,
23-
val stream: Boolean = false,
24-
val options: OllamaOptions = OllamaOptions(),
25-
)
19+
@Serializable data class ChatMessage(val role: String, val content: String)
2620

27-
@Serializable data class OllamaOptions(val temperature: Double = 0.7, val num_predict: Int = -1)
21+
@Serializable data class ChatRequest(val model: String, val messages: List<ChatMessage>, val stream: Boolean = false)
2822

29-
@Serializable
30-
data class OllamaResponse(val model: String? = null, val response: String? = null, val done: Boolean = false)
23+
@Serializable data class ChatResponse(val message: ChatMessage? = null)
3124

3225
/**
3326
* Functional service for interacting with the Ollama model API. All dependencies are explicitly injected and immutable.
@@ -46,26 +39,39 @@ data class ModelContextService(
4639
suspend fun generateResponse(prompt: String): String {
4740
return try {
4841
logger.info(
49-
"""|Sending request to Ollama with prompt:
50-
|${prompt}..."""
42+
"""|Sending chat request to Ollama with prompt:
43+
|
44+
|$prompt..."""
5145
.trimMargin()
5246
)
53-
val request = OllamaRequest(model = config.modelName, prompt = prompt)
54-
val ollamaApiUrl = "${config.modelApiUrl}/generate"
47+
val request =
48+
ChatRequest(
49+
model = config.modelName,
50+
messages =
51+
listOf(
52+
ChatMessage(role = "system", content = "You are a helpful assistant who explains software codebases."),
53+
ChatMessage(role = "user", content = prompt),
54+
),
55+
stream = false,
56+
)
57+
58+
val ollamaApiUrl = "${config.modelApiUrl}/chat"
5559
val httpResponse = sendRequest(ollamaApiUrl, request)
5660

5761
if (!httpResponse.status.isSuccess()) {
5862
val errorBody = httpResponse.bodyAsText()
5963
logger.error("Ollama API error: ${httpResponse.status} - $errorBody")
6064
"API error (${httpResponse.status}): $errorBody"
6165
} else {
62-
val response = httpResponse.body<OllamaResponse>()
66+
val response = httpResponse.body<ChatResponse>()
67+
val reply = response.message?.content?.trim() ?: "No reply received"
6368
logger.info(
64-
"""|Received response from Ollama:
65-
|${response.response}"""
69+
"""|Received reply from Ollama:
70+
|
71+
|${reply}"""
6672
.trimMargin()
6773
)
68-
response.response ?: "No response generated"
74+
reply
6975
}
7076
} catch (e: Exception) {
7177
logger.error("Error generating response: ${e.message}", e)
@@ -76,66 +82,83 @@ data class ModelContextService(
7682
/**
7783
* Build a prompt for the model context based on the provided README file.
7884
*
79-
* @param readme List of code snippets from the repository to analyze
85+
* @param codeSnippets List of code snippets from the repository to analyze
86+
* @param readme Content of the README file
8087
* @return A structured prompt for the model
8188
*/
82-
fun buildInsightsPrompt(readme: String) =
83-
"""|You are an expert codebase analyst with deep expertise in software architecture, secure and scalable system design, and programming languages including Java, Kotlin, Python, Go, Scala, JavaScript, TypeScript, C++, Rust, Ruby, and others.
89+
fun buildInsightsPrompt(codeSnippets: List<String>, readme: String) =
90+
"""|You are analyzing a software codebase that includes a README file and source code files. Your task is to extract a structured summary of the codebase’s architecture, components, and relationships.
8491
|
85-
|You will be provided with the README file of a repository. Based on the README **alone**, provide a comprehensive analysis covering the following aspects:
92+
|Use the information provided below.
8693
|
87-
|1. **Overall architecture** — inferred from descriptions, diagrams, setup steps, or configuration details.
88-
|2. **Primary programming languages** — identify the main languages used and describe how they interact if applicable.
89-
|3. **Key components and dependencies** — identify modules, services, tools, or third-party integrations and their relationships.
90-
|4. **Design patterns** — mention any explicitly referenced or implicitly suggested architectural or code patterns.
91-
|5. **Code quality signals** — identify any potential issues or areas of improvement (e.g., based on structure, naming, tooling).
92-
|6. **Security considerations** — highlight any security best practices followed or missing (e.g., credential handling, auth mechanisms).
93-
|7. **Performance considerations** — discuss caching, concurrency, resource management, or deployment implications.
94-
|8. **Language-specific practices** — note idiomatic usage or violations of best practices for the identified languages.
94+
|----------------------
95+
|README Content:
9596
|
96-
|If any of the above are not explicitly described, provide clearly labeled **inferences** based on available information.
97+
|~~~markdown
98+
|${readme.replace("```", "~~~")}
99+
|~~~
97100
|
98-
|Format your response in markdown using clear sections. Include direct references to specific README content where relevant. If multiple languages are involved, explain any cross-language integration points.
101+
|----------------------
102+
|Code Snippets:
99103
|
100-
|README Content:
101-
|~~~markdown
102-
|${readme.replace("```","~~~")}
103-
|~~~"""
104+
|${codeSnippets.joinToString("\n\n")}
105+
|----------------------
106+
|
107+
|For each file:
108+
|- File name and language
109+
|- Main classes, functions, or data structures
110+
|- Purpose of the file (based on comments, function names, etc.)
111+
|- Key public interfaces (functions, methods, classes)
112+
|- If applicable, how this file connects to other parts of the codebase (e.g. imports, API usage, function calls)
113+
|
114+
|Use this format:
115+
|
116+
|### File: path/to/file.ext (Language: X)
117+
|- **Purpose**: ...
118+
|- **Key Components**:
119+
| - ...
120+
|- **Relationships**:
121+
| - ...
122+
|
123+
|Repeat this for all files. Avoid speculation. Be concise and grounded in the content above.
124+
|"""
104125
.trimMargin()
105126

106127
/**
107128
* Build a summary prompt for the model context based on the provided code structure and snippets.
108129
*
109-
* @param codeStructure Map representing the structure of the codebase
110-
* @param codeSnippets List of code snippets from the repository
130+
* @param insights From the first Prompt step
111131
* @return A structured prompt for the model
112132
*/
113-
fun buildSummaryPrompt(codeStructure: Map<String, Any>, codeSnippets: List<String>): String =
114-
"""|You are analyzing a software code repository. You are provided with the following information:
133+
fun buildSummaryPrompt(insights: String): String =
134+
"""|You are writing a high-level but technically accurate summary of a software codebase for a developer unfamiliar with the project.
115135
|
116-
|**Code Structure:**
117-
|${codeStructure.entries.joinToString("\n") { "${it.key}: ${it.value}" }}
136+
|Use the extracted structural analysis below to build the summary.
118137
|
119-
|**Code Snippets:**
120-
|${codeSnippets.joinToString("\n\n")}
138+
|----------------------
121139
|
122-
|Using this information, write a comprehensive and accessible summary of the codebase. Your goal is to help a technically proficient developer who is new to the project quickly understand its structure and purpose.
140+
|Structural Analysis:
123141
|
124-
|Your summary must cover the following aspects:
142+
|${parseInsights(insights)}
143+
|----------------------
125144
|
126-
|1. **Main purpose** of the project
127-
|2. **Core architecture and components**
128-
|3. **Technologies and programming languages** used
129-
|4. **Key functionality and workflows**
130-
|5. **Potential areas for improvement or refactoring**
145+
|You must include:
131146
|
132-
|Where helpful, include **brief illustrative code snippets** from the examples provided to clarify key concepts, architectural decisions, or coding patterns.
147+
|1. **Main Purpose** – what the software does, who might use it.
148+
|2. **Architecture Overview** – main components and how they interact.
149+
|3. **Technologies and Languages** – what languages, frameworks, or tools are used.
150+
|4. **Key Workflows** – example flow of data, processing, or control (e.g., "HTTP request -> router -> controller -> database").
151+
|5. **Strengths and Weaknesses** – any observed complexity, tight coupling, lack of documentation, or reusable design patterns.
133152
|
134-
|Format your response using markdown with clear section headings and concise, informative language. Avoid speculation beyond the provided inputs unless clearly stated as inference.
135-
|"""
153+
|Where helpful, include:
154+
|- Short code snippets or function/method names (language-agnostic).
155+
|- Architecture patterns if detected (e.g., MVC, pub/sub, monolith, microservice).
156+
|
157+
|Format the output using Markdown. Be concise and insightful.
158+
"""
136159
.trimMargin()
137160

138-
private suspend fun sendRequest(url: String, request: OllamaRequest): HttpResponse {
161+
private suspend fun sendRequest(url: String, request: ChatRequest): HttpResponse {
139162
return httpClient.post(url) {
140163
contentType(ContentType.Application.Json)
141164
setBody(request)
@@ -162,10 +185,16 @@ data class ModelContextService(
162185
)
163186
}
164187
install(HttpTimeout) {
165-
requestTimeoutMillis = 10.minutes.inWholeMilliseconds
166-
socketTimeoutMillis = 10.minutes.inWholeMilliseconds
188+
requestTimeoutMillis = 60.minutes.inWholeMilliseconds
189+
socketTimeoutMillis = 60.minutes.inWholeMilliseconds
167190
connectTimeoutMillis = 120_000
168191
}
169192
}
193+
194+
fun parseInsights(insights: String): String {
195+
val lines = insights.lines()
196+
val fileIndex = lines.indexOfFirst { it.trimStart().startsWith("### File:") }
197+
return if (fileIndex != -1) lines.drop(fileIndex).joinToString("\n") else insights
198+
}
170199
}
171200
}

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

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package mcp.code.analysis.service
22

3-
import kotlinx.coroutines.*
4-
53
/** Service for analyzing Git repositories. */
64
data class RepositoryAnalysisService(
75
private val gitService: GitService = GitService(),
@@ -15,26 +13,20 @@ data class RepositoryAnalysisService(
1513
* @param branch The branch of the repository to analyze.
1614
* @return A summary of the analysis results.
1715
*/
18-
fun analyzeRepository(repoUrl: String, branch: String): String {
16+
suspend fun analyzeRepository(repoUrl: String, branch: String): String {
1917
return try {
2018
val repoDir = gitService.cloneRepository(repoUrl, branch)
2119

22-
val readmeContent = codeAnalyzer.findReadmeFile(repoDir)
23-
val codeStructure = codeAnalyzer.analyzeStructure(repoDir)
20+
val readme = codeAnalyzer.findReadmeFile(repoDir)
2421
val codeSnippets = codeAnalyzer.collectAllCodeSnippets(repoDir)
2522

26-
val insightsPrompt = modelContextService.buildInsightsPrompt(readmeContent)
27-
val summaryPrompt = modelContextService.buildSummaryPrompt(codeStructure, codeSnippets)
23+
val insightsPrompt = modelContextService.buildInsightsPrompt(codeSnippets, readme)
24+
val insightsResponse = modelContextService.generateResponse(insightsPrompt)
25+
26+
val summaryPrompt = modelContextService.buildSummaryPrompt(insightsResponse)
27+
val summaryResponse = modelContextService.generateResponse(summaryPrompt)
2828

29-
runBlocking {
30-
val insights = async { modelContextService.generateResponse(insightsPrompt) }
31-
val summary = async { modelContextService.generateResponse(summaryPrompt) }
32-
"""|${insights.await()}
33-
|
34-
|${summary.await()}
35-
|"""
36-
.trimMargin()
37-
}
29+
summaryResponse
3830
} catch (e: Exception) {
3931
throw Exception("Error analyzing repository: ${e.message}", e)
4032
}

src/main/resources/logback.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<configuration>
3-
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
3+
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
44

55
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
66
<target>System.err</target>
@@ -28,8 +28,8 @@
2828
<appender-ref ref="FILE"/>
2929
</root>
3030

31-
<logger name="io.modelcontextprotocol" level="WARN"/> <!-- Changed to catch all MCP packages -->
31+
<logger name="io.modelcontextprotocol" level="WARN"/>
3232
<logger name="io.netty" level="INFO"/>
3333
<logger name="org.eclipse.jetty" level="INFO"/>
34-
<logger name="ch.qos.logback" level="WARN"/> <!-- Reduce internal logback messages -->
34+
<logger name="ch.qos.logback" level="WARN"/>
3535
</configuration>

0 commit comments

Comments
 (0)