From e7b7834991f2a0a57dc439b47ebf23c8e12b78e3 Mon Sep 17 00:00:00 2001 From: mariano Date: Tue, 2 Sep 2025 22:27:34 -0500 Subject: [PATCH 1/4] feat: mcp server enhancements --- .../processor/CodeContentProcessor.kt | 70 ++- .../kotlin/mcp/code/analysis/server/Mcp.kt | 50 +- .../CodeContentProcessorPropertyTest.kt | 534 ++++++++++++++++++ 3 files changed, 604 insertions(+), 50 deletions(-) create mode 100644 src/test/kotlin/mcp/code/analysis/processor/CodeContentProcessorPropertyTest.kt diff --git a/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt b/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt index 2c85f8e..ca235fd 100644 --- a/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt +++ b/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt @@ -17,28 +17,54 @@ internal class CodeContentProcessor(private val patterns: LanguagePatterns, priv * @return A list of lines selected for summarization. */ fun processContent(lines: List): List { - val finalState = - lines.fold(ProcessingState()) { state, line -> - if (state.lines.size >= maxLines) return@fold state - - val trimmed = line.trim() - val nextInCommentBlock = determineCommentBlockState(trimmed, state.inCommentBlock) - val shouldIncludeLine = isDefinition(line) || isCommentLine(line) || state.inCommentBlock - - val updatedLines = - if (shouldIncludeLine) { - when { - isDefinition(line) -> state.lines + processDefinitionLine(line) - else -> state.lines + line - } - } else { - state.lines - } - - ProcessingState(updatedLines, nextInCommentBlock) - } - - return finalState.lines + if (lines.isEmpty()) return emptyList() + + // First pass: decide which original lines should be included, tracking comment block state + val includeFlags = BooleanArray(lines.size) + var inCommentBlock = false + lines.forEachIndexed { idx, line -> + val trimmed = line.trim() + val shouldInclude = isDefinition(line) || isCommentLine(line) || inCommentBlock + includeFlags[idx] = shouldInclude + val nextInCommentBlock = determineCommentBlockState(trimmed, inCommentBlock) + inCommentBlock = nextInCommentBlock + } + + // Second pass: build output with explicit separators between non-contiguous regions + val result = mutableListOf() + var lastIncludedIndex = -2 // ensure the first included line does not trigger separator logic + + fun maybeAddSeparator(nextIndex: Int): Boolean { + // Return true if a separator was added + if (result.isEmpty()) return false + val isGap = nextIndex != lastIncludedIndex + 1 + if (!isGap) return false + // Ensure there is room for the separator and at least one code line + if (result.size + 2 > maxLines) return false + result.add("...") + return true + } + + for (i in lines.indices) { + if (!includeFlags[i]) continue + + // If there is a gap from the last included line, insert a separator if we have room + maybeAddSeparator(i) + + // Prepare the line to add, possibly normalizing definitions + val line = lines[i] + val toAdd = if (isDefinition(line)) processDefinitionLine(line) else line + + // Respect maxLines strictly, prioritizing code lines over separators + if (result.size >= maxLines) break + + result.add(toAdd) + lastIncludedIndex = i + + if (result.size >= maxLines) break + } + + return result } private fun isDefinition(line: String): Boolean = patterns.definitionPattern.containsMatchIn(line.trim()) diff --git a/src/main/kotlin/mcp/code/analysis/server/Mcp.kt b/src/main/kotlin/mcp/code/analysis/server/Mcp.kt index f913bcc..85cbb50 100644 --- a/src/main/kotlin/mcp/code/analysis/server/Mcp.kt +++ b/src/main/kotlin/mcp/code/analysis/server/Mcp.kt @@ -15,9 +15,12 @@ import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport import io.modelcontextprotocol.kotlin.sdk.server.mcp +import kotlin.collections.get +import kotlin.collections.remove +import kotlin.text.get +import kotlin.text.set import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.io.IOException import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered @@ -85,59 +88,50 @@ class Mcp( val server = configureServer() servers[transport.sessionId] = server - val heartbeatJob = launch { - flow { - while (true) { - emit(Unit) - delay(15_000) - } - } - .onEach { send(ServerSentEvent(event = "heartbeat")) } - .catch { e -> - when (e) { - is IOException -> logger.debug("Client disconnected during heartbeat: ${e.message}") - else -> logger.error("Heartbeat error: ${e.message}", e) - } - } - .onCompletion { logger.debug("Heartbeat job terminated for session: ${transport.sessionId}") } - .collect() - } - server.onClose { - logger.info("Server closed") + logger.info("Server closed for session: ${transport.sessionId}") servers.remove(transport.sessionId) } - server.connect(transport) - try { + server.connect(transport) + logger.info("Server connected for session: ${transport.sessionId}") awaitCancellation() + } catch (e: Exception) { + logger.error("Connection error: ${e.message}", e) } finally { - heartbeatJob.cancel() + servers.remove(transport.sessionId) logger.info("SSE connection closed for session: ${transport.sessionId}") } } post("/message") { try { - val sessionId = call.request.queryParameters["sessionId"] ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing sessionId parameter") - val transport = servers[sessionId]?.transport as? SseServerTransport - if (transport == null) { + val server = servers[sessionId] + if (server == null) { + logger.warn("Session not found: $sessionId") call.respond(HttpStatusCode.NotFound, "Session not found") return@post } + val transport = server.transport as? SseServerTransport + if (transport == null) { + logger.warn("Invalid transport for session: $sessionId") + call.respond(HttpStatusCode.InternalServerError, "Invalid transport") + return@post + } + logger.debug("Handling message for session: $sessionId") - transport.handlePostMessage(call) + withTimeout(3_600_000) { 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}") + call.respond(HttpStatusCode.InternalServerError, "Error: ${e.message}") } } } diff --git a/src/test/kotlin/mcp/code/analysis/processor/CodeContentProcessorPropertyTest.kt b/src/test/kotlin/mcp/code/analysis/processor/CodeContentProcessorPropertyTest.kt new file mode 100644 index 0000000..b2e1526 --- /dev/null +++ b/src/test/kotlin/mcp/code/analysis/processor/CodeContentProcessorPropertyTest.kt @@ -0,0 +1,534 @@ +package mcp.code.analysis.processor + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContain +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.checkAll + +class CodeContentProcessorPropertyTest : + StringSpec({ + // Build the same language patterns used by CodeAnalyzer + fun createLanguagePatterns(): Map = + mapOf( + "kotlin" to + LanguagePatterns( + Regex( + "\\b(class|interface|object|enum\\s+class|data\\s+class|sealed\\s+class|fun|val|var|const|typealias|annotation\\s+class|import|package)\\b" + ), + listOf("//"), + "/*", + "*/", + ), + "scala" to + LanguagePatterns( + Regex( + "\\b(class|object|trait|case\\s+class|case\\s+object|def|val|var|lazy\\s+val|type|implicit|sealed|abstract|override|package\\s+object|import|package)\\b" + ), + listOf("//"), + "/*", + "*/", + ), + "java" to + LanguagePatterns( + Regex( + "\\b(class|interface|enum|@interface|record|public|private|protected|static|abstract|final|synchronized|volatile|native|transient|strictfp|void|import|package)\\b" + ), + listOf("//"), + "/*", + "*/", + ), + "python" to + LanguagePatterns( + Regex("\\b(def|class|async\\s+def)\\b|@\\w+|\\b(import|from)\\b"), + listOf("#"), + "\"\"\"", + "\"\"\"", + ), + "ruby" to + LanguagePatterns( + Regex("\\b(def|class|module|attr_\\w+|require|include|extend)\\b"), + listOf("#"), + "=begin", + "=end", + ), + "javascript" to + LanguagePatterns( + Regex("\\b(function|class|const|let|var|import|export|interface|type|enum|namespace)\\b"), + listOf("//"), + "/*", + "*/", + ), + "typescript" to + LanguagePatterns( + Regex("\\b(function|class|const|let|var|import|export|interface|type|enum|namespace)\\b"), + listOf("//"), + "/*", + "*/", + ), + "go" to + LanguagePatterns( + Regex("\\b(func|type|struct|interface|package|import|var|const)\\b"), + listOf("//"), + "/*", + "*/", + ), + "rust" to + LanguagePatterns( + Regex("\\b(fn|struct|enum|trait|impl|pub|use|mod|const|static|type|async|unsafe)\\b"), + listOf("//"), + "/*", + "*/", + ), + "c" to + LanguagePatterns( + Regex("\\b(struct|enum|typedef|void|int|char|bool|extern|static|class)\\b"), + listOf("//"), + "/*", + "*/", + ), + "cpp" to + LanguagePatterns( + Regex("\\b(class|struct|enum|typedef|namespace|template|void|int|char|bool|auto|extern|static|virtual)\\b"), + listOf("//"), + "/*", + "*/", + ), + "default" to + LanguagePatterns( + Regex("\\b(class|interface|object|enum|fun|def|function|public|private|protected|static)\\b"), + listOf("//", "#"), + "/*", + "*/", + ), + ) + + val langPatterns = createLanguagePatterns() + + // Language generator (same set tested in CodeAnalyzerPropertyTest) + val languageGenerator = arbitrary { + listOf("kotlin", "java", "scala", "python", "ruby", "javascript", "typescript", "go", "c", "cpp", "rust").random() + } + + // Generate code content for different languages (duplicated from CodeAnalyzerPropertyTest) + fun generateCodeForLanguage(language: String): String { + return when (language) { + "kotlin" -> + """ + package test + + // This is a comment + /* Block comment + with multiple lines */ + class TestClass { + fun testMethod() { + // Method comment + val x = 1 + } + } + + object Singleton { + val constant = 42 + } + """ + .trimIndent() + + "java" -> + """ + package test; + + // This is a comment + /* Block comment + with multiple lines */ + public class TestClass { + // Field comment + private int field; + + public void testMethod() { + // Method comment + int x = 1; + } + } + + interface TestInterface { + void testMethod(); + } + """ + .trimIndent() + + "scala" -> + """ + package test + + // This is a comment + /* Block comment + with multiple lines */ + class TestClass { + def testMethod(): Unit = { + // Method comment + val x = 1 + } + } + + object Singleton { + val constant = 42 + } + + trait TestTrait { + def abstractMethod(): Unit + } + """ + .trimIndent() + + "python" -> + """ + # This is a comment + + \"\"\" + Block comment + with multiple lines + \"\"\" + + def test_function(): + # Function comment + x = 1 + + class TestClass: + \"\"\"Class docstring\"\"\" + def __init__(self): + self.value = 42 + + def method(self): + return self.value + """ + .trimIndent() + + "ruby" -> + """ + # This is a comment + + =begin + Block comment + with multiple lines + =end + + def test_method + # Method comment + x = 1 + end + + class TestClass + def initialize + @value = 42 + end + + def method + @value + end + end + + module TestModule + def self.module_method + puts "Hello" + end + end + """ + .trimIndent() + + "javascript", + "typescript" -> + """ + // This is a comment + + /* Block comment + with multiple lines */ + + function testFunction() { + // Function comment + const x = 1; + } + + class TestClass { + constructor() { + this.value = 42; + } + + method() { + return this.value; + } + } + + const arrowFn = () => { + return "Hello"; + }; + """ + .trimIndent() + + "go" -> + """ + package test + + // This is a comment + + /* Block comment + with multiple lines */ + + func testFunction() { + // Function comment + x := 1 + } + + type TestStruct struct { + Value int + } + + func (t TestStruct) Method() int { + return t.Value + } + + type TestInterface interface { + Method() int + } + """ + .trimIndent() + + "c", + "cpp" -> + """ + // This is a comment + + /* Block comment + with multiple lines */ + + void testFunction() { + // Function comment + int x = 1; + } + + struct TestStruct { + int value; + }; + + class TestClass { + public: + TestClass() : value(42) {} + + int method() { + return value; + } + + private: + int value; + }; + """ + .trimIndent() + + "rust" -> + """ + // This is a comment + + /* Block comment + with multiple lines */ + + fn test_function() { + // Function comment + let x = 1; + } + + struct TestStruct { + value: i32, + } + + impl TestStruct { + fn new() -> Self { + TestStruct { value: 42 } + } + + fn method(&self) -> i32 { + self.value + } + } + + trait TestTrait { + fn trait_method(&self) -> i32; + } + """ + .trimIndent() + + else -> + """ + """ + .trimIndent() + } + } + + fun containsSubstring(lines: List, substr: String): Boolean = lines.any { it.contains(substr) } + + "processContent should extract definitions and comments for all languages" { + checkAll(50, languageGenerator) { language -> + val patterns = langPatterns[language] ?: error("Missing patterns for $language") + val processor = CodeContentProcessor(patterns, 100) + val content = generateCodeForLanguage(language) + val result = processor.processContent(content.lines()) + + when (language) { + "kotlin" -> { + assert(containsSubstring(result, "class TestClass")) + assert(containsSubstring(result, "fun testMethod")) + assert(containsSubstring(result, "object Singleton")) + assert(containsSubstring(result, "// This is a comment")) + assert(containsSubstring(result, "/* Block comment")) + } + "java" -> { + assert(containsSubstring(result, "public class TestClass")) + assert(containsSubstring(result, "interface TestInterface")) + assert(containsSubstring(result, "// This is a comment")) + assert(containsSubstring(result, "/* Block comment")) + } + "scala" -> { + assert(containsSubstring(result, "class TestClass")) + assert(containsSubstring(result, "def testMethod")) + assert(containsSubstring(result, "object Singleton")) + assert(containsSubstring(result, "trait TestTrait")) + assert(containsSubstring(result, "// This is a comment")) + } + "python" -> { + assert(containsSubstring(result, "def test_function")) + assert(containsSubstring(result, "class TestClass")) + assert(containsSubstring(result, "# This is a comment")) + } + "ruby" -> { + assert(containsSubstring(result, "def test_method")) + assert(containsSubstring(result, "class TestClass")) + assert(containsSubstring(result, "module TestModule")) + assert(containsSubstring(result, "# This is a comment")) + } + "javascript", + "typescript" -> { + assert(containsSubstring(result, "function testFunction")) + assert(containsSubstring(result, "class TestClass")) + assert(containsSubstring(result, "const arrowFn")) + assert(containsSubstring(result, "// This is a comment")) + } + "go" -> { + assert(containsSubstring(result, "func testFunction")) + assert(containsSubstring(result, "type TestStruct struct")) + assert(containsSubstring(result, "type TestInterface interface")) + assert(containsSubstring(result, "// This is a comment")) + } + "c", + "cpp" -> { + assert(containsSubstring(result, "void testFunction")) + assert(containsSubstring(result, "struct TestStruct")) + assert(containsSubstring(result, "class TestClass")) + assert(containsSubstring(result, "// This is a comment")) + } + "rust" -> { + assert(containsSubstring(result, "fn test_function")) + assert(containsSubstring(result, "struct TestStruct")) + assert(containsSubstring(result, "impl TestStruct")) + assert(containsSubstring(result, "trait TestTrait")) + assert(containsSubstring(result, "// This is a comment")) + } + } + } + } + + "processContent should respect maxLines parameter" { + checkAll(50, languageGenerator) { language -> + val patterns = langPatterns[language] ?: error("Missing patterns for $language") + val processor = CodeContentProcessor(patterns, 10) + val largeContent = generateCodeForLanguage(language).repeat(10) + val result = processor.processContent(largeContent.lines()) + assert(result.size <= 10) { "Result should have at most 10 lines, but had ${'$'}{result.size}" } + } + } + + "processContent should handle empty content" { + checkAll(20, languageGenerator) { language -> + val patterns = langPatterns[language] ?: error("Missing patterns for $language") + val processor = CodeContentProcessor(patterns, 100) + val result = processor.processContent(emptyList()) + assert(result.isEmpty()) { "Expected empty result for empty input" } + } + } + + "processContent should handle content with only comments" { + checkAll(20, languageGenerator) { language -> + val patterns = langPatterns[language] ?: error("Missing patterns for $language") + val processor = CodeContentProcessor(patterns, 100) + val commentOnlyContent = + if (language in listOf("python", "ruby")) "# This is only a comment\n# Another comment line" + else "// This is only a comment\n// Another comment line" + val result = processor.processContent(commentOnlyContent.lines()) + result.shouldContain(commentOnlyContent.lines()[0]) + result.shouldContain(commentOnlyContent.lines()[1]) + } + } + + "processContent should handle content with only definitions" { + checkAll(20, languageGenerator) { language -> + val patterns = langPatterns[language] ?: error("Missing patterns for $language") + val processor = CodeContentProcessor(patterns, 100) + val definitionOnlyContent = + when (language) { + "kotlin" -> "class Test {}\nfun testMethod() {}" + "java" -> "public class Test {}\npublic void testMethod() {}" + "scala" -> "class Test {}\ndef testMethod() {}" + "python" -> "class Test:\n pass\ndef test_function():\n pass" + "ruby" -> "class Test\nend\ndef test_method\nend" + "javascript", + "typescript" -> "class Test {}\nfunction testMethod() {}" + "go" -> "type Test struct {}\nfunc testFunction() {}" + "c", + "cpp" -> "struct Test {};\nvoid testFunction() {}" + "rust" -> "struct Test {}\nfn test_function() {}" + else -> "class Test {}\nfunction testMethod() {}" + } + val result = processor.processContent(definitionOnlyContent.lines()) + when (language) { + "kotlin" -> { + assert(containsSubstring(result, "class Test")) + assert(containsSubstring(result, "fun testMethod")) + } + "java" -> { + assert(containsSubstring(result, "public class Test")) + assert(containsSubstring(result, "public void testMethod")) + } + "scala" -> { + assert(containsSubstring(result, "class Test")) + assert(containsSubstring(result, "def testMethod")) + } + "python" -> { + assert(containsSubstring(result, "class Test")) + assert(containsSubstring(result, "def test_function")) + } + "ruby" -> { + assert(containsSubstring(result, "class Test")) + assert(containsSubstring(result, "def test_method")) + } + "javascript", + "typescript" -> { + assert(containsSubstring(result, "class Test")) + assert(containsSubstring(result, "function testMethod")) + } + "go" -> { + assert(containsSubstring(result, "type Test struct")) + assert(containsSubstring(result, "func testFunction")) + } + "c", + "cpp" -> { + assert(containsSubstring(result, "struct Test")) + assert(containsSubstring(result, "void testFunction")) + } + "rust" -> { + assert(containsSubstring(result, "struct Test")) + assert(containsSubstring(result, "fn test_function")) + } + else -> { + assert(containsSubstring(result, "class Test")) + assert(containsSubstring(result, "function testMethod")) + } + } + } + } + }) From 3ab8b94c62e2ad65567b54d120c6d8b3a67c8f14 Mon Sep 17 00:00:00 2001 From: mariano Date: Tue, 2 Sep 2025 22:30:09 -0500 Subject: [PATCH 2/4] feat: mcp server enhancements --- .../processor/CodeContentProcessor.kt | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt b/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt index ca235fd..840488a 100644 --- a/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt +++ b/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt @@ -13,13 +13,22 @@ internal class CodeContentProcessor(private val patterns: LanguagePatterns, priv * Processes a list of code lines and returns those that should be included in the summary. Lines are included if they * are definitions, comments, or part of a block comment. * + * The processing follows a two-pass approach: + * 1. First pass: Decides which original lines should be included, tracking comment block state + * 2. Second pass: Builds output with explicit separators between non-contiguous regions + * + * During the first pass, lines are included if they are definitions, comments, or part of a block comment. The + * comment block state is tracked to ensure multi-line comments are properly captured. + * + * During the second pass, separators ("...") are added between non-contiguous regions to indicate omitted code + * sections. The maxLines limit is strictly respected, prioritizing code lines over separators. + * * @param lines The lines of code to process. - * @return A list of lines selected for summarization. + * @return A list of lines selected for summarization, with separators indicating omitted sections. */ fun processContent(lines: List): List { if (lines.isEmpty()) return emptyList() - // First pass: decide which original lines should be included, tracking comment block state val includeFlags = BooleanArray(lines.size) var inCommentBlock = false lines.forEachIndexed { idx, line -> @@ -30,16 +39,13 @@ internal class CodeContentProcessor(private val patterns: LanguagePatterns, priv inCommentBlock = nextInCommentBlock } - // Second pass: build output with explicit separators between non-contiguous regions val result = mutableListOf() - var lastIncludedIndex = -2 // ensure the first included line does not trigger separator logic + var lastIncludedIndex = -2 fun maybeAddSeparator(nextIndex: Int): Boolean { - // Return true if a separator was added if (result.isEmpty()) return false val isGap = nextIndex != lastIncludedIndex + 1 if (!isGap) return false - // Ensure there is room for the separator and at least one code line if (result.size + 2 > maxLines) return false result.add("...") return true @@ -48,14 +54,11 @@ internal class CodeContentProcessor(private val patterns: LanguagePatterns, priv for (i in lines.indices) { if (!includeFlags[i]) continue - // If there is a gap from the last included line, insert a separator if we have room maybeAddSeparator(i) - // Prepare the line to add, possibly normalizing definitions val line = lines[i] val toAdd = if (isDefinition(line)) processDefinitionLine(line) else line - // Respect maxLines strictly, prioritizing code lines over separators if (result.size >= maxLines) break result.add(toAdd) From 2ed769c6afcbfa07d58caf81986350c47ae2c5b9 Mon Sep 17 00:00:00 2001 From: mariano Date: Tue, 2 Sep 2025 23:20:19 -0500 Subject: [PATCH 3/4] feat: mcp server enhancements --- .../mcp/code/analysis/processor/CodeAnalyzer.kt | 1 - src/main/kotlin/mcp/code/analysis/server/Mcp.kt | 11 ++++------- .../kotlin/mcp/code/analysis/config/AppConfigTest.kt | 2 +- .../code/analysis/service/ModelContextServiceTest.kt | 2 +- .../analysis/service/RepositoryAnalysisServiceTest.kt | 1 - 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/mcp/code/analysis/processor/CodeAnalyzer.kt b/src/main/kotlin/mcp/code/analysis/processor/CodeAnalyzer.kt index 3982efc..f728eda 100644 --- a/src/main/kotlin/mcp/code/analysis/processor/CodeAnalyzer.kt +++ b/src/main/kotlin/mcp/code/analysis/processor/CodeAnalyzer.kt @@ -1,7 +1,6 @@ package mcp.code.analysis.processor import java.io.File -import kotlin.text.lines import mcp.code.analysis.service.ModelContextService import org.slf4j.Logger import org.slf4j.LoggerFactory diff --git a/src/main/kotlin/mcp/code/analysis/server/Mcp.kt b/src/main/kotlin/mcp/code/analysis/server/Mcp.kt index 85cbb50..c610baf 100644 --- a/src/main/kotlin/mcp/code/analysis/server/Mcp.kt +++ b/src/main/kotlin/mcp/code/analysis/server/Mcp.kt @@ -7,7 +7,6 @@ import io.ktor.server.engine.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.sse.* -import io.ktor.sse.* import io.ktor.util.collections.* import io.modelcontextprotocol.kotlin.sdk.* import io.modelcontextprotocol.kotlin.sdk.server.Server as SdkServer @@ -15,12 +14,10 @@ import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.SseServerTransport import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport import io.modelcontextprotocol.kotlin.sdk.server.mcp -import kotlin.collections.get -import kotlin.collections.remove -import kotlin.text.get -import kotlin.text.set -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered diff --git a/src/test/kotlin/mcp/code/analysis/config/AppConfigTest.kt b/src/test/kotlin/mcp/code/analysis/config/AppConfigTest.kt index 149353c..05b2d77 100644 --- a/src/test/kotlin/mcp/code/analysis/config/AppConfigTest.kt +++ b/src/test/kotlin/mcp/code/analysis/config/AppConfigTest.kt @@ -1,6 +1,6 @@ package mcp.code.analysis.config -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class AppConfigTest { diff --git a/src/test/kotlin/mcp/code/analysis/service/ModelContextServiceTest.kt b/src/test/kotlin/mcp/code/analysis/service/ModelContextServiceTest.kt index d80c1f6..5005954 100644 --- a/src/test/kotlin/mcp/code/analysis/service/ModelContextServiceTest.kt +++ b/src/test/kotlin/mcp/code/analysis/service/ModelContextServiceTest.kt @@ -5,7 +5,7 @@ import io.ktor.client.engine.mock.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* -import io.mockk.* +import io.mockk.mockk import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import mcp.code.analysis.config.AppConfig diff --git a/src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt b/src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt index f782ec9..97f4083 100644 --- a/src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt +++ b/src/test/kotlin/mcp/code/analysis/service/RepositoryAnalysisServiceTest.kt @@ -5,7 +5,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import java.io.File -import java.lang.Exception import kotlin.test.assertEquals import kotlinx.coroutines.test.runTest import mcp.code.analysis.processor.CodeAnalyzer From 7511963417816d8925f9cbae02ffb3a197b9266a Mon Sep 17 00:00:00 2001 From: mariano Date: Tue, 2 Sep 2025 23:36:28 -0500 Subject: [PATCH 4/4] feat: mcp server enhancements --- .../code/analysis/processor/CodeAnalyzer.kt | 19 +++--- .../processor/CodeContentProcessor.kt | 68 ++++++++++--------- .../service/RepositoryAnalysisService.kt | 30 ++++---- 3 files changed, 57 insertions(+), 60 deletions(-) diff --git a/src/main/kotlin/mcp/code/analysis/processor/CodeAnalyzer.kt b/src/main/kotlin/mcp/code/analysis/processor/CodeAnalyzer.kt index f728eda..17dd711 100644 --- a/src/main/kotlin/mcp/code/analysis/processor/CodeAnalyzer.kt +++ b/src/main/kotlin/mcp/code/analysis/processor/CodeAnalyzer.kt @@ -12,8 +12,6 @@ data class LanguagePatterns( val blockCommentEnd: String, ) -data class ProcessingState(val lines: List = emptyList(), val inCommentBlock: Boolean = false) - /** * Responsible for analyzing the structure of a codebase. Identifies files, directories, and their respective metadata * such as size, language, imports, and declarations. @@ -125,15 +123,14 @@ data class CodeAnalyzer( findCodeFiles(repoDir) .filter { it.isRelevantCodeFile() && !it.isTestFile() } .mapNotNull { file -> - try { - val relativePath = file.getRelativePathFrom(repoDir) - val language = getLanguageFromExtension(file.extension) - val content = file.readText() - summarizeCodeContent(relativePath, language, content, maxLines) - } catch (e: Exception) { - logger.warn("Error processing file ${file.absolutePath}: ${e.message}") - null - } + runCatching { + val relativePath = file.getRelativePathFrom(repoDir) + val language = getLanguageFromExtension(file.extension) + val content = file.readText() + summarizeCodeContent(relativePath, language, content, maxLines) + } + .onFailure { e -> logger.warn("Error processing file ${file.absolutePath}: ${e.message}") } + .getOrNull() } .also { logCollectionResults(it, repoDir) } diff --git a/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt b/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt index 840488a..79462fe 100644 --- a/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt +++ b/src/main/kotlin/mcp/code/analysis/processor/CodeContentProcessor.kt @@ -29,45 +29,51 @@ internal class CodeContentProcessor(private val patterns: LanguagePatterns, priv fun processContent(lines: List): List { if (lines.isEmpty()) return emptyList() - val includeFlags = BooleanArray(lines.size) - var inCommentBlock = false - lines.forEachIndexed { idx, line -> - val trimmed = line.trim() - val shouldInclude = isDefinition(line) || isCommentLine(line) || inCommentBlock - includeFlags[idx] = shouldInclude - val nextInCommentBlock = determineCommentBlockState(trimmed, inCommentBlock) - inCommentBlock = nextInCommentBlock + // First pass: compute inclusion flags functionally while tracking the comment block state + data class Pass1(val flags: MutableList, val inBlock: Boolean) + + val pass1 = + lines.foldIndexed(Pass1(mutableListOf(), false)) { idx, acc, line -> + val trimmed = line.trim() + val shouldInclude = isDefinition(line) || isCommentLine(line) || acc.inBlock + acc.flags.add(shouldInclude) + val nextInCommentBlock = determineCommentBlockState(trimmed, acc.inBlock) + acc.copy(flags = acc.flags, inBlock = nextInCommentBlock) + } + + val includeFlags: List = pass1.flags + + // Second pass: build output with separators between non-contiguous regions + // Accumulates second-pass output and the index of the last included source line + data class OutputAcc(val result: MutableList, val lastIdx: Int) + + fun maybeAddSeparatorFn(state: OutputAcc, nextIndex: Int): OutputAcc { + if (state.result.isEmpty()) return state + val isGap = nextIndex != state.lastIdx + 1 + if (!isGap) return state + if (state.result.size + 2 > maxLines) return state + state.result.add("...") + return state } - val result = mutableListOf() - var lastIncludedIndex = -2 + val finalAcc: OutputAcc = + lines.indices.fold(OutputAcc(mutableListOf(), -2)) { acc, i -> + if (!includeFlags[i]) return@fold acc - fun maybeAddSeparator(nextIndex: Int): Boolean { - if (result.isEmpty()) return false - val isGap = nextIndex != lastIncludedIndex + 1 - if (!isGap) return false - if (result.size + 2 > maxLines) return false - result.add("...") - return true - } - - for (i in lines.indices) { - if (!includeFlags[i]) continue + val afterSep = maybeAddSeparatorFn(acc, i) - maybeAddSeparator(i) + val line = lines[i] + val toAdd = if (isDefinition(line)) processDefinitionLine(line) else line - val line = lines[i] - val toAdd = if (isDefinition(line)) processDefinitionLine(line) else line + if (afterSep.result.size >= maxLines) return@fold afterSep - if (result.size >= maxLines) break + afterSep.result.add(toAdd) + val updated = afterSep.copy(result = afterSep.result, lastIdx = i) - result.add(toAdd) - lastIncludedIndex = i - - if (result.size >= maxLines) break - } + if (updated.result.size >= maxLines) updated else updated + } - return result + return finalAcc.result } private fun isDefinition(line: String): Boolean = patterns.definitionPattern.containsMatchIn(line.trim()) diff --git a/src/main/kotlin/mcp/code/analysis/service/RepositoryAnalysisService.kt b/src/main/kotlin/mcp/code/analysis/service/RepositoryAnalysisService.kt index da65f1e..5b34fe2 100644 --- a/src/main/kotlin/mcp/code/analysis/service/RepositoryAnalysisService.kt +++ b/src/main/kotlin/mcp/code/analysis/service/RepositoryAnalysisService.kt @@ -16,22 +16,16 @@ data class RepositoryAnalysisService( * @param branch The branch of the repository to analyze. * @return A summary of the analysis results. */ - suspend fun analyzeRepository(repoUrl: String, branch: String): String { - return try { - val repoDir = gitService.cloneRepository(repoUrl, branch) - - val readme = codeAnalyzer.findReadmeFile(repoDir) - val codeSnippets = codeAnalyzer.collectSummarizedCodeSnippets(repoDir) - - val insightsPrompt = modelContextService.buildInsightsPrompt(codeSnippets, readme) - val insightsResponse = modelContextService.generateResponse(insightsPrompt) - - val summaryPrompt = modelContextService.buildSummaryPrompt(insightsResponse) - val summaryResponse = modelContextService.generateResponse(summaryPrompt) - - summaryResponse - } catch (e: Exception) { - throw Exception("Error analyzing repository: ${e.message}", e) - } - } + suspend fun analyzeRepository(repoUrl: String, branch: String): String = + runCatching { gitService.cloneRepository(repoUrl, branch) } + .mapCatching { repoDir -> + val readme = codeAnalyzer.findReadmeFile(repoDir) + val codeSnippets = codeAnalyzer.collectSummarizedCodeSnippets(repoDir) + val insightsPrompt = modelContextService.buildInsightsPrompt(codeSnippets, readme) + insightsPrompt + } + .mapCatching { insightsPrompt -> modelContextService.generateResponse(insightsPrompt) } + .mapCatching { insightsResponse -> modelContextService.buildSummaryPrompt(insightsResponse) } + .mapCatching { summaryPrompt -> modelContextService.generateResponse(summaryPrompt) } + .getOrElse { e -> throw Exception("Error analyzing repository: ${e.message}", e) } }