Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ version = rootProject.scmVersion.version
description = "MCP Server for GitHub Code Repositories Analysis"

dependencies {
val ktorVersion = "3.1.3"
val ktorVersion = "3.0.2"
val coroutinesVersion = "1.10.2"

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

// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
Expand Down
56 changes: 41 additions & 15 deletions src/main/kotlin/mcp/code/analysis/server/Server.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import io.ktor.server.engine.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.sse.*
import io.ktor.sse.ServerSentEvent
import io.ktor.util.collections.*
import io.modelcontextprotocol.kotlin.sdk.*
import io.modelcontextprotocol.kotlin.sdk.server.*
import io.modelcontextprotocol.kotlin.sdk.server.Server as SdkServer
import kotlin.text.get
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.io.asSink
import kotlinx.io.asSource
Expand Down Expand Up @@ -73,13 +77,18 @@ open class Server(

embeddedServer(CIO, host = "0.0.0.0", port = port) {
install(SSE)

routing {
sse("/sse") {
val transport = SseServerTransport("/message", this)
val server: SdkServer = configureServer()
launch {
while (true) {
send(ServerSentEvent(event = "heartbeat"))
delay(15_000)
}
}

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

servers[transport.sessionId] = server

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

server.connect(transport)
}

post("/message") {
logger.info("Received Message")
val sessionId: String = call.request.queryParameters["sessionId"]!!
val transport = servers[sessionId]?.transport as? SseServerTransport
if (transport == null) {
call.respond(HttpStatusCode.NotFound, "Session not found")
return@post
}
try {

val sessionId =
call.request.queryParameters["sessionId"]
?: return@post call.respond(HttpStatusCode.BadRequest, "Missing sessionId parameter")

transport.handlePostMessage(call)
val transport = servers[sessionId]?.transport as? SseServerTransport
if (transport == null) {
call.respond(HttpStatusCode.NotFound, "Session not found")
return@post
}

logger.debug("Handling message for session: $sessionId")
transport.handlePostMessage(call)

call.respond(HttpStatusCode.OK)
} catch (e: Exception) {
logger.error("Error handling message: ${e.message}", e)
call.respond(HttpStatusCode.InternalServerError, "Error handling message: ${e.message}")
}
}
}
}
Expand All @@ -112,10 +133,15 @@ open class Server(
* @param port The port number on which the SSE MCP server will listen for client connections.
*/
open fun runSseMcpServerUsingKtorPlugin(port: Int): Unit = runBlocking {
logger.info("Starting SSE server on port $port")
logger.info("Use inspector to connect to http://localhost:$port/sse")
logger.debug("Starting SSE server on port $port")
logger.debug("Use inspector to connect to http://localhost:$port/sse")

embeddedServer(CIO, host = "0.0.0.0", port = port) { mcp { configureServer() } }.start(wait = true)
embeddedServer(CIO, host = "0.0.0.0", port = port) {
mcp {
return@mcp configureServer()
}
}
.start(wait = true)
}

/**
Expand Down
3 changes: 1 addition & 2 deletions src/main/kotlin/mcp/code/analysis/service/CodeAnalyzer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ data class CodeAnalyzer(
val relativePath = file.absolutePath.substring(repoDir.absolutePath.length + 1)
val lang = getLanguageFromExtension(file.extension)
val content = file.readLines().joinToString("\n")
"""|---
|File: $relativePath
"""|--- File: $relativePath
|~~~$lang
|$content
|~~~"""
Expand Down
143 changes: 86 additions & 57 deletions src/main/kotlin/mcp/code/analysis/service/ModelContextService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,11 @@ import mcp.code.analysis.config.AppConfig
import org.slf4j.Logger
import org.slf4j.LoggerFactory

@Serializable
data class OllamaRequest(
val model: String,
val prompt: String,
val stream: Boolean = false,
val options: OllamaOptions = OllamaOptions(),
)
@Serializable data class ChatMessage(val role: String, val content: String)

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

@Serializable
data class OllamaResponse(val model: String? = null, val response: String? = null, val done: Boolean = false)
@Serializable data class ChatResponse(val message: ChatMessage? = null)

/**
* Functional service for interacting with the Ollama model API. All dependencies are explicitly injected and immutable.
Expand All @@ -46,26 +39,39 @@ data class ModelContextService(
suspend fun generateResponse(prompt: String): String {
return try {
logger.info(
"""|Sending request to Ollama with prompt:
|${prompt}..."""
"""|Sending chat request to Ollama with prompt:
|
|$prompt..."""
.trimMargin()
)
val request = OllamaRequest(model = config.modelName, prompt = prompt)
val ollamaApiUrl = "${config.modelApiUrl}/generate"
val request =
ChatRequest(
model = config.modelName,
messages =
listOf(
ChatMessage(role = "system", content = "You are a helpful assistant who explains software codebases."),
ChatMessage(role = "user", content = prompt),
),
stream = false,
)

val ollamaApiUrl = "${config.modelApiUrl}/chat"
val httpResponse = sendRequest(ollamaApiUrl, request)

if (!httpResponse.status.isSuccess()) {
val errorBody = httpResponse.bodyAsText()
logger.error("Ollama API error: ${httpResponse.status} - $errorBody")
"API error (${httpResponse.status}): $errorBody"
} else {
val response = httpResponse.body<OllamaResponse>()
val response = httpResponse.body<ChatResponse>()
val reply = response.message?.content?.trim() ?: "No reply received"
logger.info(
"""|Received response from Ollama:
|${response.response}"""
"""|Received reply from Ollama:
|
|${reply}"""
.trimMargin()
)
response.response ?: "No response generated"
reply
}
} catch (e: Exception) {
logger.error("Error generating response: ${e.message}", e)
Expand All @@ -76,66 +82,83 @@ data class ModelContextService(
/**
* Build a prompt for the model context based on the provided README file.
*
* @param readme List of code snippets from the repository to analyze
* @param codeSnippets List of code snippets from the repository to analyze
* @param readme Content of the README file
* @return A structured prompt for the model
*/
fun buildInsightsPrompt(readme: String) =
"""|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.
fun buildInsightsPrompt(codeSnippets: List<String>, readme: String) =
"""|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.
|
|You will be provided with the README file of a repository. Based on the README **alone**, provide a comprehensive analysis covering the following aspects:
|Use the information provided below.
|
|1. **Overall architecture** — inferred from descriptions, diagrams, setup steps, or configuration details.
|2. **Primary programming languages** — identify the main languages used and describe how they interact if applicable.
|3. **Key components and dependencies** — identify modules, services, tools, or third-party integrations and their relationships.
|4. **Design patterns** — mention any explicitly referenced or implicitly suggested architectural or code patterns.
|5. **Code quality signals** — identify any potential issues or areas of improvement (e.g., based on structure, naming, tooling).
|6. **Security considerations** — highlight any security best practices followed or missing (e.g., credential handling, auth mechanisms).
|7. **Performance considerations** — discuss caching, concurrency, resource management, or deployment implications.
|8. **Language-specific practices** — note idiomatic usage or violations of best practices for the identified languages.
|----------------------
|README Content:
|
|If any of the above are not explicitly described, provide clearly labeled **inferences** based on available information.
|~~~markdown
|${readme.replace("```", "~~~")}
|~~~
|
|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.
|----------------------
|Code Snippets:
|
|README Content:
|~~~markdown
|${readme.replace("```","~~~")}
|~~~"""
|${codeSnippets.joinToString("\n\n")}
|----------------------
|
|For each file:
|- File name and language
|- Main classes, functions, or data structures
|- Purpose of the file (based on comments, function names, etc.)
|- Key public interfaces (functions, methods, classes)
|- If applicable, how this file connects to other parts of the codebase (e.g. imports, API usage, function calls)
|
|Use this format:
|
|### File: path/to/file.ext (Language: X)
|- **Purpose**: ...
|- **Key Components**:
| - ...
|- **Relationships**:
| - ...
|
|Repeat this for all files. Avoid speculation. Be concise and grounded in the content above.
|"""
.trimMargin()

/**
* Build a summary prompt for the model context based on the provided code structure and snippets.
*
* @param codeStructure Map representing the structure of the codebase
* @param codeSnippets List of code snippets from the repository
* @param insights From the first Prompt step
* @return A structured prompt for the model
*/
fun buildSummaryPrompt(codeStructure: Map<String, Any>, codeSnippets: List<String>): String =
"""|You are analyzing a software code repository. You are provided with the following information:
fun buildSummaryPrompt(insights: String): String =
"""|You are writing a high-level but technically accurate summary of a software codebase for a developer unfamiliar with the project.
|
|**Code Structure:**
|${codeStructure.entries.joinToString("\n") { "${it.key}: ${it.value}" }}
|Use the extracted structural analysis below to build the summary.
|
|**Code Snippets:**
|${codeSnippets.joinToString("\n\n")}
|----------------------
|
|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.
|Structural Analysis:
|
|Your summary must cover the following aspects:
|${parseInsights(insights)}
|----------------------
|
|1. **Main purpose** of the project
|2. **Core architecture and components**
|3. **Technologies and programming languages** used
|4. **Key functionality and workflows**
|5. **Potential areas for improvement or refactoring**
|You must include:
|
|Where helpful, include **brief illustrative code snippets** from the examples provided to clarify key concepts, architectural decisions, or coding patterns.
|1. **Main Purpose** – what the software does, who might use it.
|2. **Architecture Overview** – main components and how they interact.
|3. **Technologies and Languages** – what languages, frameworks, or tools are used.
|4. **Key Workflows** – example flow of data, processing, or control (e.g., "HTTP request -> router -> controller -> database").
|5. **Strengths and Weaknesses** – any observed complexity, tight coupling, lack of documentation, or reusable design patterns.
|
|Format your response using markdown with clear section headings and concise, informative language. Avoid speculation beyond the provided inputs unless clearly stated as inference.
|"""
|Where helpful, include:
|- Short code snippets or function/method names (language-agnostic).
|- Architecture patterns if detected (e.g., MVC, pub/sub, monolith, microservice).
|
|Format the output using Markdown. Be concise and insightful.
"""
.trimMargin()

private suspend fun sendRequest(url: String, request: OllamaRequest): HttpResponse {
private suspend fun sendRequest(url: String, request: ChatRequest): HttpResponse {
return httpClient.post(url) {
contentType(ContentType.Application.Json)
setBody(request)
Expand All @@ -162,10 +185,16 @@ data class ModelContextService(
)
}
install(HttpTimeout) {
requestTimeoutMillis = 10.minutes.inWholeMilliseconds
socketTimeoutMillis = 10.minutes.inWholeMilliseconds
requestTimeoutMillis = 60.minutes.inWholeMilliseconds
socketTimeoutMillis = 60.minutes.inWholeMilliseconds
connectTimeoutMillis = 120_000
}
}

fun parseInsights(insights: String): String {
val lines = insights.lines()
val fileIndex = lines.indexOfFirst { it.trimStart().startsWith("### File:") }
return if (fileIndex != -1) lines.drop(fileIndex).joinToString("\n") else insights
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package mcp.code.analysis.service

import kotlinx.coroutines.*

/** Service for analyzing Git repositories. */
data class RepositoryAnalysisService(
private val gitService: GitService = GitService(),
Expand All @@ -15,26 +13,20 @@ data class RepositoryAnalysisService(
* @param branch The branch of the repository to analyze.
* @return A summary of the analysis results.
*/
fun analyzeRepository(repoUrl: String, branch: String): String {
suspend fun analyzeRepository(repoUrl: String, branch: String): String {
return try {
val repoDir = gitService.cloneRepository(repoUrl, branch)

val readmeContent = codeAnalyzer.findReadmeFile(repoDir)
val codeStructure = codeAnalyzer.analyzeStructure(repoDir)
val readme = codeAnalyzer.findReadmeFile(repoDir)
val codeSnippets = codeAnalyzer.collectAllCodeSnippets(repoDir)

val insightsPrompt = modelContextService.buildInsightsPrompt(readmeContent)
val summaryPrompt = modelContextService.buildSummaryPrompt(codeStructure, codeSnippets)
val insightsPrompt = modelContextService.buildInsightsPrompt(codeSnippets, readme)
val insightsResponse = modelContextService.generateResponse(insightsPrompt)

val summaryPrompt = modelContextService.buildSummaryPrompt(insightsResponse)
val summaryResponse = modelContextService.generateResponse(summaryPrompt)

runBlocking {
val insights = async { modelContextService.generateResponse(insightsPrompt) }
val summary = async { modelContextService.generateResponse(summaryPrompt) }
"""|${insights.await()}
|
|${summary.await()}
|"""
.trimMargin()
}
summaryResponse
} catch (e: Exception) {
throw Exception("Error analyzing repository: ${e.message}", e)
}
Expand Down
6 changes: 3 additions & 3 deletions src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>

<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
Expand Down Expand Up @@ -28,8 +28,8 @@
<appender-ref ref="FILE"/>
</root>

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