[MCP Server] Add advanced tests for all toolsets. Improve transport tests

(cherry picked from commit bcb3c215ddcccfad3311ed5163503175e54254a4)

IJ-CR-166188

GitOrigin-RevId: 0a16d89676ee5f7d9f80688805bd4312d7f20ccd
This commit is contained in:
Artem.Bukhonov
2025-06-17 19:59:34 +02:00
committed by intellij-monorepo-bot
parent 23483ce45d
commit efe41f6d8d
15 changed files with 758 additions and 62 deletions

View File

@@ -14,6 +14,7 @@ class ReflectionCallableMcpTool(override val descriptor: McpToolDescriptor, priv
return when { return when {
result.result == null -> McpToolCallResult.text("[null]") result.result == null -> McpToolCallResult.text("[null]")
result.result is Unit -> McpToolCallResult.text("[success]") result.result is Unit -> McpToolCallResult.text("[success]")
result.result is String -> McpToolCallResult.text(result.result) // special case for String to avoid extra quotes added by Any?.toString()
result.result.javaClass.isPrimitive -> McpToolCallResult.text(result.result.toString()) result.result.javaClass.isPrimitive -> McpToolCallResult.text(result.result.toString())
result.result is McpToolCallResult -> result.result result.result is McpToolCallResult -> result.result
result.result is McpToolCallResultContent -> McpToolCallResult(arrayOf(result.result), isError = false) result.result is McpToolCallResultContent -> McpToolCallResult(arrayOf(result.result), isError = false)

View File

@@ -4,6 +4,7 @@ import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.serializerOrNull import kotlinx.serialization.serializerOrNull
import java.lang.reflect.InvocationTargetException
import kotlin.reflect.KCallable import kotlin.reflect.KCallable
import kotlin.reflect.KParameter import kotlin.reflect.KParameter
import kotlin.reflect.KType import kotlin.reflect.KType
@@ -44,7 +45,12 @@ class CallableBridge(private val callable: KCallable<*>, private val thisRef: An
argMap[parameter] = decodedArg argMap[parameter] = decodedArg
} }
val result = callable.callSuspendBy(argMap) val result = try {
callable.callSuspendBy(argMap)
}
catch (e: InvocationTargetException) {
throw e.cause ?: e
}
return Result(result, callable.returnType, json) return Result(result, callable.returnType, json)
} }
} }

View File

@@ -7,8 +7,9 @@ import com.intellij.mcpserver.annotations.McpDescription
import com.intellij.mcpserver.annotations.McpTool import com.intellij.mcpserver.annotations.McpTool
import com.intellij.mcpserver.project import com.intellij.mcpserver.project
import com.intellij.mcpserver.util.resolveRel import com.intellij.mcpserver.util.resolveRel
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.invokeLater import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.application.writeAction
import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.project.guessProjectDir
@@ -18,7 +19,9 @@ import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.breakpoints.XLineBreakpoint import com.intellij.xdebugger.breakpoints.XLineBreakpoint
import com.intellij.xdebugger.impl.XSourcePositionImpl import com.intellij.xdebugger.impl.XSourcePositionImpl
import com.intellij.xdebugger.impl.breakpoints.XBreakpointUtil import com.intellij.xdebugger.impl.breakpoints.XBreakpointUtil
import kotlin.coroutines.coroutineContext import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.withContext
class DebuggerToolset : McpToolset { class DebuggerToolset : McpToolset {
@McpTool @McpTool
@@ -39,23 +42,23 @@ class DebuggerToolset : McpToolset {
@McpDescription("Line number where to toggle the breakpoint (1-based)") @McpDescription("Line number where to toggle the breakpoint (1-based)")
line: Int line: Int
): String { ): String {
val project = coroutineContext.project val project = currentCoroutineContext().project
val projectDir = project.guessProjectDir()?.toNioPathOrNull() val projectDir = project.guessProjectDir()?.toNioPathOrNull()
?: return "can't find project dir" ?: return "can't find project dir"
val virtualFile = LocalFileSystem.getInstance().findFileByNioFile(projectDir.resolveRel(filePathInProject)) val virtualFile = LocalFileSystem.getInstance().findFileByNioFile(projectDir.resolveRel(filePathInProject))
?: return "can't find file '$filePathInProject'" ?: return "can't find file '$filePathInProject'"
val editors = FileEditorManager.getInstance(project).openFile(virtualFile, true) val editors = withContext(Dispatchers.EDT) { FileEditorManager.getInstance (project).openFile(virtualFile, true) }
val editor = editors.filterIsInstance<TextEditor>().firstOrNull()?.editor val editor = editors.filterIsInstance<TextEditor>().firstOrNull()?.editor
?: return "can't find editor for file '$filePathInProject'" ?: return "can't find editor for file '$filePathInProject'"
runWriteAction { writeAction {
val position = XSourcePositionImpl.create(virtualFile, line - 1) val position = XSourcePositionImpl.create(virtualFile, line - 1)
XBreakpointUtil.toggleLineBreakpoint(project, position, false, editor, false, true, true).onSuccess { XBreakpointUtil.toggleLineBreakpoint(project, position, false, editor, false, true, true).onSuccess {
invokeLater { invokeLater {
position.createNavigatable(project).navigate(true) position.createNavigatable(project).navigate(true)
} }
} }
} }
return "ok" return "ok"
} }
@@ -71,7 +74,7 @@ class DebuggerToolset : McpToolset {
Note: Only includes line breakpoints, not other breakpoint types (e.g., method breakpoints) Note: Only includes line breakpoints, not other breakpoint types (e.g., method breakpoints)
""") """)
suspend fun get_debugger_breakpoints(): String { suspend fun get_debugger_breakpoints(): String {
val project = coroutineContext.project val project = currentCoroutineContext().project
val breakpointManager = XDebuggerManager.getInstance(project).breakpointManager val breakpointManager = XDebuggerManager.getInstance(project).breakpointManager
return breakpointManager.allBreakpoints return breakpointManager.allBreakpoints
.filterIsInstance<XLineBreakpoint<*>>() // Only consider line breakpoints .filterIsInstance<XLineBreakpoint<*>>() // Only consider line breakpoints

View File

@@ -2,6 +2,7 @@
package com.intellij.mcpserver.toolsets.terminal package com.intellij.mcpserver.toolsets.terminal
import com.intellij.mcpserver.McpExpectedError
import com.intellij.mcpserver.McpServerBundle import com.intellij.mcpserver.McpServerBundle
import com.intellij.mcpserver.McpToolset import com.intellij.mcpserver.McpToolset
import com.intellij.mcpserver.annotations.McpDescription import com.intellij.mcpserver.annotations.McpDescription
@@ -37,7 +38,8 @@ class TerminalToolset : McpToolset {
suspend fun get_terminal_text(): String { suspend fun get_terminal_text(): String {
val project = coroutineContext.project val project = coroutineContext.project
val text = runReadAction<String?> { val text = runReadAction<String?> {
TerminalView.getInstance(project).getWidgets().firstOrNull()?.text val terminalWidget = TerminalView.getInstance(project).getWidgets().firstOrNull() ?: throw McpExpectedError("No terminal available")
terminalWidget.text
} }
return text ?: "" return text ?: ""
} }

View File

@@ -1,69 +1,31 @@
package com.intellij.mcpserver package com.intellij.mcpserver
import com.intellij.mcpserver.impl.McpServerService
import com.intellij.mcpserver.stdio.IJ_MCP_SERVER_PORT import com.intellij.mcpserver.stdio.IJ_MCP_SERVER_PORT
import com.intellij.mcpserver.stdio.IJ_MCP_SERVER_PROJECT_PATH import com.intellij.mcpserver.stdio.IJ_MCP_SERVER_PROJECT_PATH
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.TestDisposable
import io.ktor.utils.io.streams.asInput
import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport
import kotlinx.coroutines.test.runTest
import kotlinx.io.asSink
import kotlinx.io.buffered
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import kotlin.test.fail
@TestApplication class McpJsonGenerationTest {
class StdioRunnerTest {
@TestDisposable
private lateinit var disposable: Disposable
@Test
fun list_tools_stdio_runner() = runTest {
McpServerService.getInstance().start()
Disposer.register(disposable, Disposable {
McpServerService.getInstance().stop()
})
val mcpServerCommandLine = createStdioMcpServerCommandLine(McpServerService.getInstance().port, null)
val processBuilder = mcpServerCommandLine.toProcessBuilder()
val process = processBuilder.start()
val stdioClientTransport = StdioClientTransport(process.inputStream.asInput(), process.outputStream.asSink().buffered())
val client = Client(Implementation(name = "test client", version = "1.0"))
client.connect(stdioClientTransport)
val listTools = client.listTools() ?: fail("No tools returned")
assert(listTools.tools.isNotEmpty()) { "No tools returned" }
stdioClientTransport.close()
if (!process.waitFor(10, java.util.concurrent.TimeUnit.SECONDS)) process.destroyForcibly()
if (!process.waitFor(10, java.util.concurrent.TimeUnit.SECONDS)) fail("Process is still alive")
}
@Test @Test
fun test_createStdioMcpServerJsonConfiguration() { fun test_createStdioMcpServerJsonConfiguration() {
val port = 12345 val port = 12345
val projectPath = "/test/project/path" val projectPath = "/test/project/path"
val jsonConfig = createStdioMcpServerJsonConfiguration(port, projectPath) val jsonConfig = createStdioMcpServerJsonConfiguration(port, projectPath)
// Verify the JSON structure // Verify the JSON structure
Assertions.assertNotNull(jsonConfig["command"]) Assertions.assertNotNull(jsonConfig["command"])
Assertions.assertNotNull(jsonConfig["args"]) Assertions.assertNotNull(jsonConfig["args"])
Assertions.assertNotNull(jsonConfig["env"]) Assertions.assertNotNull(jsonConfig["env"])
// Verify command is a java executable // Verify command is a java executable
val command = jsonConfig["command"]?.jsonPrimitive?.content val command = jsonConfig["command"]?.jsonPrimitive?.content
assertTrue(command?.endsWith("java") == true, "Command should be java executable") assertTrue(command?.endsWith("java") == true, "Command should be java executable")
// Verify environment variables // Verify environment variables
val env = jsonConfig["env"]?.jsonObject!! val env = jsonConfig["env"]?.jsonObject!!
Assertions.assertNotNull(env) Assertions.assertNotNull(env)
@@ -74,9 +36,9 @@ class StdioRunnerTest {
@Test @Test
fun test_createStdioMcpServerJsonConfiguration_withoutProjectPath() { fun test_createStdioMcpServerJsonConfiguration_withoutProjectPath() {
val port = 12345 val port = 12345
val jsonConfig = createStdioMcpServerJsonConfiguration(port, null) val jsonConfig = createStdioMcpServerJsonConfiguration(port, null)
// Verify the JSON structure // Verify the JSON structure
Assertions.assertNotNull(jsonConfig["command"]) Assertions.assertNotNull(jsonConfig["command"])
Assertions.assertNotNull(jsonConfig["args"]) Assertions.assertNotNull(jsonConfig["args"])
@@ -94,9 +56,9 @@ class StdioRunnerTest {
@Test @Test
fun test_createSseServerJsonEntry() { fun test_createSseServerJsonEntry() {
val port = 8080 val port = 8080
val jsonConfig = createSseServerJsonEntry(port) val jsonConfig = createSseServerJsonEntry(port)
// Verify the JSON structure // Verify the JSON structure
Assertions.assertEquals("sse", jsonConfig["type"]?.jsonPrimitive?.content) Assertions.assertEquals("sse", jsonConfig["type"]?.jsonPrimitive?.content)
Assertions.assertEquals("http://localhost:$port/sse", jsonConfig["url"]?.jsonPrimitive?.content) Assertions.assertEquals("http://localhost:$port/sse", jsonConfig["url"]?.jsonPrimitive?.content)

View File

@@ -0,0 +1,95 @@
@file:Suppress("TestFunctionName")
package com.intellij.mcpserver
import com.intellij.mcpserver.impl.McpServerService
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.fixture.moduleFixture
import com.intellij.testFramework.junit5.fixture.projectFixture
import com.intellij.testFramework.junit5.fixture.sourceRootFixture
import com.intellij.testFramework.junit5.fixture.virtualFileFixture
import io.ktor.client.HttpClient
import io.ktor.client.plugins.sse.SSE
import io.modelcontextprotocol.kotlin.sdk.CallToolResultBase
import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.modelcontextprotocol.kotlin.sdk.TextContent
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.platform.commons.annotation.Testable
import kotlin.test.fail
@Testable
@TestApplication
abstract class McpToolsetTestBase {
protected val projectFixture = projectFixture(openAfterCreation = true)
protected val project by projectFixture
protected val moduleFixture = projectFixture.moduleFixture("testModule")
protected val sourceRootFixture = moduleFixture.sourceRootFixture()
// TODO: no idea how to create a file in a subfolder
protected val mainJavaFileFixture = sourceRootFixture.virtualFileFixture("Main.java", "Main.java content")
protected val classJavaFileFixture = sourceRootFixture.virtualFileFixture("Class.java", "Class.java content")
protected val testJavaFileFixture = sourceRootFixture.virtualFileFixture("Test.java", "Test.java content")
protected val mainJavaFile by mainJavaFileFixture
protected val classJavaFile by classJavaFileFixture
protected val testJavaFile by testJavaFileFixture
protected suspend fun withConnection(action: suspend (Client) -> Unit) {
McpServerService.getInstance().start()
// Get the port from McpServerService
val port = McpServerService.getInstance().port
// Create HttpClient with SSE support
val httpClient = HttpClient {
install(SSE)
}
// Create SseClientTransport
val sseClientTransport = SseClientTransport(httpClient, "http://localhost:$port/")
// Create client
val client = Client(Implementation(name = "test client", version = "1.0"))
try { // Connect to the server
client.connect(sseClientTransport)
action(client)
}
finally { // Close the connection
sseClientTransport.close()
McpServerService.getInstance().stop()
}
}
protected val CallToolResultBase.textContent: TextContent get() = content.firstOrNull() as? TextContent
?: fail("Tool call result should be TextContent")
protected suspend fun testMcpTool(
toolName: String,
input: kotlinx.serialization.json.JsonObject,
output: String,
) {
testMcpTool(toolName, input) { result ->
val textContent = result.textContent
assertEquals(output, textContent.text, "Tool call result should match expected output")
}
}
protected suspend fun testMcpTool(
toolName: String,
input: kotlinx.serialization.json.JsonObject,
resultChecker: (CallToolResultBase) -> Unit,
) {
withConnection { client ->
// Call the tool with the provided input
val result = client.callTool(toolName, input) ?: fail("Tool call result should not be null")
resultChecker(result)
// Just verify that the call doesn't throw an exception
assertNotNull(result, "Tool call result should not be null")
// Log the result for debugging
println("[DEBUG_LOG] Tool $toolName result: $result")
}
}
}

View File

@@ -0,0 +1,134 @@
package com.intellij.mcpserver
import com.intellij.mcpserver.annotations.McpDescription
import com.intellij.mcpserver.impl.McpServerService
import com.intellij.mcpserver.impl.util.asTool
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.use
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.fixture.projectFixture
import com.intellij.util.application
import io.ktor.client.HttpClient
import io.ktor.client.plugins.sse.SSE
import io.ktor.utils.io.streams.asInput
import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport
import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlinx.io.asSink
import kotlinx.io.buffered
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.fail
@TestApplication
class TransportTest {
companion object {
val projectFixture = projectFixture(openAfterCreation = true)
val project by projectFixture
@JvmStatic
fun getTransports(): Array<TransportHolder> {
return arrayOf(
StdioTransportHolder(project),
SseTransportHolder(project),
)
}
}
@ParameterizedTest
@MethodSource("getTransports")
fun list_tools(transport: TransportHolder) = transportTest(transport) { client ->
val listTools = client.listTools() ?: fail("No tools returned")
assert(listTools.tools.isNotEmpty()) { "No tools returned" }
}
@ParameterizedTest
@MethodSource("getTransports")
fun tool_call_has_project(transport: TransportHolder) = transportTest(transport) { client ->
Disposer.newDisposable().use { disposable ->
application.extensionArea.getExtensionPoint(McpToolsProvider.EP).registerExtension(object : McpToolsProvider {
override fun getTools(): List<McpTool> {
return listOf(this@TransportTest::test_tool.asTool())
}
}, disposable)
client.callTool("test_tool", emptyMap())
val actual = withTimeout(2000) { projectFromTool.await() }
assertEquals(project, actual)
}
}
val projectFromTool = CompletableDeferred<Project?>()
@com.intellij.mcpserver.annotations.McpTool()
@McpDescription("Test description")
suspend fun test_tool() {
projectFromTool.complete(currentCoroutineContext().projectOrNull)
}
private fun transportTest(transportHolder: TransportHolder, action: suspend (Client) -> Unit) = runBlocking {
try {
McpServerService.getInstance().start()
val client = Client(Implementation(name = "test client", version = "1.0"))
client.connect(transportHolder.transport)
action(client)
}
finally {
transportHolder.close()
McpServerService.getInstance().stop()
}
}
}
abstract class TransportHolder {
abstract val transport: AbstractTransport
// do not make it AutoCloseable because Junit tries to close it automatically but we want to close it in test method manually
open fun close() {
runBlocking {
transport.close()
}
}
}
class StdioTransportHolder(project: Project) : TransportHolder() {
val process: Process by lazy {
createStdioMcpServerCommandLine(McpServerService.getInstance().port, project.basePath).toProcessBuilder().start()
}
override val transport: AbstractTransport by lazy {
StdioClientTransport(process.inputStream.asInput(), process.outputStream.asSink().buffered())
}
override fun close() {
super.close() //sseClientTransport.close()
if (!process.waitFor(10, TimeUnit.SECONDS)) process.destroyForcibly()
if (!process.waitFor(10, TimeUnit.SECONDS)) fail("Process is still alive")
}
override fun toString(): String {
return "Stdio"
}
}
class SseTransportHolder(project: Project) : TransportHolder() {
override val transport: AbstractTransport by lazy {
SseClientTransport(HttpClient {
install(SSE)
}, "http://localhost:${McpServerService.getInstance().port}/")
}
override fun toString(): String {
return "SSE"
}
}

View File

@@ -0,0 +1,108 @@
@file:Suppress("TestFunctionName")
package com.intellij.mcpserver.toolsets
import com.intellij.mcpserver.McpToolsetTestBase
import com.intellij.mcpserver.toolsets.general.BuiltinGeneralToolset
import io.kotest.common.runBlocking
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
class BuiltinGeneralToolsetTest : McpToolsetTestBase() {
@Disabled("Depends on the previous runs that may create files via fixture")
@Test
fun search_in_files_content() = runBlocking {
testMcpTool(
BuiltinGeneralToolset::search_in_files_content.name,
buildJsonObject {
put("searchText", JsonPrimitive("content"))
},
"""[{"path": "Main.java", "name": "Main.java"},
{"path": "Class.java", "name": "Class.java"},
{"path": "Test.java", "name": "Test.java"}]"""
)
}
@Test
fun get_run_configurations() = runBlocking {
testMcpTool(
BuiltinGeneralToolset::get_run_configurations.name,
buildJsonObject {},
"[]"
)
}
@Test
fun run_configuration() = runBlocking {
testMcpTool(
BuiltinGeneralToolset::run_configuration.name,
buildJsonObject {
put("configName", JsonPrimitive("test-config"))
},
"Run configuration with name 'test-config' not found."
)
}
@Test
fun get_project_modules() = runBlocking {
testMcpTool(
BuiltinGeneralToolset::get_project_modules.name,
buildJsonObject {},
"[testModule]"
)
}
@Test
fun get_project_dependencies() = runBlocking {
testMcpTool(
BuiltinGeneralToolset::get_project_dependencies.name,
buildJsonObject {},
"[]"
)
}
@Test
fun list_available_actions() = runBlocking {
testMcpTool(
BuiltinGeneralToolset::list_available_actions.name,
buildJsonObject {}
) {
assert(it.textContent.text?.contains(""""id": "About"""") == true) { "'About' action not found"}
}
}
@Test
fun execute_action_by_id() = runBlocking {
testMcpTool(
BuiltinGeneralToolset::execute_action_by_id.name,
buildJsonObject {
put("actionId", JsonPrimitive("SaveAll"))
},
"ok"
)
}
@Disabled("IMHO Useless tool. To remove")
@Test
fun get_progress_indicators() = runBlocking {
testMcpTool(
BuiltinGeneralToolset::get_progress_indicators.name,
buildJsonObject {},
"[]"
)
}
@Test
@Disabled("Imho useless tool. To remove")
fun wait_tool() = runBlocking {
testMcpTool(
BuiltinGeneralToolset::wait.name,
buildJsonObject {
put("milliseconds", JsonPrimitive(100)) // Short wait for testing
},
"ok"
)
}
}

View File

@@ -0,0 +1,36 @@
@file:Suppress("TestFunctionName")
package com.intellij.mcpserver.toolsets
import com.intellij.mcpserver.McpToolsetTestBase
import com.intellij.mcpserver.toolsets.debugger.DebuggerToolset
import io.kotest.common.runBlocking
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
class DebuggerToolsetTest : McpToolsetTestBase() {
@Test
fun get_debugger_breakpoints() = runBlocking {
testMcpTool(
DebuggerToolset::get_debugger_breakpoints.name,
buildJsonObject {},
"[]"
)
}
@Disabled("Tool doesn't work")
@Test
fun toggle_debugger_breakpoint() = runBlocking {
testMcpTool(
DebuggerToolset::toggle_debugger_breakpoint.name,
buildJsonObject {
put("filePathInProject", JsonPrimitive("Main.java"))
put("line", JsonPrimitive(0))
},
"[]"
)
}
}

View File

@@ -0,0 +1,36 @@
@file:Suppress("TestFunctionName")
package com.intellij.mcpserver.toolsets
import com.intellij.mcpserver.McpToolsetTestBase
import com.intellij.mcpserver.toolsets.general.ErrorToolset
import com.intellij.openapi.application.EDT
import com.intellij.openapi.fileEditor.FileEditorManager
import io.kotest.common.runBlocking
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.buildJsonObject
import org.junit.jupiter.api.Test
class ErrorToolsetTest : McpToolsetTestBase() {
@Test
fun get_current_file_errors() = runBlocking {
withContext(Dispatchers.EDT) {
FileEditorManager.getInstance(project).openFile(mainJavaFile, true)
}
testMcpTool(
ErrorToolset::get_current_file_errors.name,
buildJsonObject {},
"[]"
)
}
@Test
fun get_project_problems() = runBlocking {
testMcpTool(
ErrorToolset::get_project_problems.name,
buildJsonObject {},
"[]"
)
}
}

View File

@@ -0,0 +1,102 @@
@file:Suppress("TestFunctionName")
package com.intellij.mcpserver.toolsets
import com.intellij.mcpserver.McpToolsetTestBase
import com.intellij.mcpserver.toolsets.general.FileToolset
import com.intellij.openapi.application.EDT
import com.intellij.openapi.fileEditor.FileEditorManager
import io.kotest.common.runBlocking
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
class FileToolsetTest : McpToolsetTestBase() {
@Disabled("Output contains the project directory name that is not predictable because of generated")
@Test
fun list_directory_tree_in_folder() = runBlocking {
testMcpTool(
FileToolset::list_directory_tree_in_folder.name,
buildJsonObject {
put("pathInProject", JsonPrimitive("/"))
put("maxDepth", JsonPrimitive(2))
},
"""{"name":"IJ4720168971772072169","type":"directory","path":"","children":[{"name":"Class.java","type":"file","path":"Class.java"},{"name":"Test.java","type":"file","path":"Test.java"},{"name":"Main.java","type":"file","path":"Main.java"}]}"""
)
}
@Test
fun list_files_in_folder() = runBlocking {
testMcpTool(
FileToolset::list_files_in_folder.name,
buildJsonObject {
put("pathInProject", JsonPrimitive("/"))
},
"""[{"name": "Class.java", "type": "file", "path": "Class.java"},
{"name": "Test.java", "type": "file", "path": "Test.java"},
{"name": "Main.java", "type": "file", "path": "Main.java"}]"""
)
}
@Test
fun find_files_by_name_substring() = runBlocking {
testMcpTool(
FileToolset::find_files_by_name_substring.name,
buildJsonObject {
put("nameSubstring", JsonPrimitive("test"))
},
"""[{"path": "Test.java", "name": "Test.java"}]"""
)
}
@Test
fun create_new_file_with_text() = runBlocking {
testMcpTool(
FileToolset::create_new_file_with_text.name,
buildJsonObject {
put("pathInProject", JsonPrimitive("test/test_file.txt"))
put("text", JsonPrimitive("This is a test file"))
},
"ok"
)
}
@Test
fun open_file_in_editor() = runBlocking {
testMcpTool(
FileToolset::open_file_in_editor.name,
buildJsonObject {
put("filePath", JsonPrimitive("Test.java"))
},
"file is opened"
)
}
@Test
fun get_all_open_file_paths() = runBlocking {
withContext(Dispatchers.EDT) {
FileEditorManager.getInstance(project).openFile(mainJavaFile, true)
}
testMcpTool(
FileToolset::get_all_open_file_paths.name,
buildJsonObject {},
"Main.java"
)
}
@Test
fun get_open_in_editor_file_path() = runBlocking {
withContext(Dispatchers.EDT) {
FileEditorManager.getInstance(project).openFile(mainJavaFile, true)
}
testMcpTool(
FileToolset::get_open_in_editor_file_path.name,
buildJsonObject {}
) { result ->
assert(result.textContent.text?.endsWith("Main.java") == true)
}
}
}

View File

@@ -0,0 +1,39 @@
@file:Suppress("TestFunctionName")
package com.intellij.mcpserver.toolsets
import com.intellij.mcpserver.McpToolsetTestBase
import com.intellij.mcpserver.toolsets.general.FormattingToolset
import com.intellij.openapi.application.EDT
import com.intellij.openapi.fileEditor.FileEditorManager
import io.kotest.common.runBlocking
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import org.junit.jupiter.api.Test
class FormattingToolsetTest : McpToolsetTestBase() {
@Test
fun reformat_current_file() = runBlocking {
withContext(Dispatchers.EDT) {
FileEditorManager.getInstance(project).openFile(mainJavaFile, true)
}
testMcpTool(
FormattingToolset::reformat_current_file.name,
buildJsonObject {},
"ok"
)
}
@Test
fun reformat_file() = runBlocking {
testMcpTool(
FormattingToolset::reformat_file.name,
buildJsonObject {
put("pathInProject", JsonPrimitive(mainJavaFile.name))
},
"ok"
)
}
}

View File

@@ -0,0 +1,32 @@
@file:Suppress("TestFunctionName")
package com.intellij.mcpserver.toolsets
import com.intellij.mcpserver.McpToolsetTestBase
import com.intellij.mcpserver.toolsets.terminal.TerminalToolset
import io.kotest.common.runBlocking
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import org.junit.jupiter.api.Test
class TerminalToolsetTest : McpToolsetTestBase() {
@Test
fun get_terminal_text() = runBlocking {
testMcpTool(
TerminalToolset::get_terminal_text.name,
buildJsonObject {},
"No terminal available"
)
}
@Test
fun execute_terminal_command() = runBlocking {
testMcpTool(
TerminalToolset::execute_terminal_command.name,
buildJsonObject {
put("command", JsonPrimitive("echo 'Hello, World!'"))
},
"No terminal available"
)
}
}

View File

@@ -0,0 +1,105 @@
@file:Suppress("TestFunctionName")
package com.intellij.mcpserver.toolsets
import com.intellij.mcpserver.McpToolsetTestBase
import com.intellij.mcpserver.toolsets.general.TextToolset
import com.intellij.openapi.application.EDT
import com.intellij.openapi.fileEditor.FileEditorManager
import io.kotest.common.runBlocking
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import org.junit.jupiter.api.Test
class TextToolsetTest : McpToolsetTestBase() {
@Test
fun get_open_in_editor_file_text() = runBlocking {
testMcpTool(
TextToolset::get_open_in_editor_file_text.name,
buildJsonObject {},
""
)
}
// TODO: IMHO badly designed tool. Can junk LLM input. To remove in favor of list_editor_files and get_text_by_path
@Test
fun get_all_open_file_texts() = runBlocking {
testMcpTool(
TextToolset::get_all_open_file_texts.name,
buildJsonObject {},
"[]"
)
}
@Test
fun get_selected_in_editor_text() = runBlocking {
testMcpTool(
TextToolset::get_selected_in_editor_text.name,
buildJsonObject {},
""
)
}
@Test
fun replace_selected_text() = runBlocking {
testMcpTool(
TextToolset::replace_selected_text.name,
buildJsonObject {
put("text", JsonPrimitive("replacement text"))
},
"no text selected"
)
}
@Test
fun replace_current_file_text() = runBlocking {
withContext(Dispatchers.EDT) {
FileEditorManager.getInstance(project).openFile(mainJavaFile, true)
}
testMcpTool(
TextToolset::replace_current_file_text.name,
buildJsonObject {
put("text", JsonPrimitive("new file content"))
},
"ok"
)
}
@Test
fun get_file_text_by_path() = runBlocking {
testMcpTool(
TextToolset::get_file_text_by_path.name,
buildJsonObject {
put("pathInProject", JsonPrimitive(testJavaFile.name))
},
"Test.java content"
)
}
@Test
fun replace_specific_text() = runBlocking {
testMcpTool(
TextToolset::replace_specific_text.name,
buildJsonObject {
put("pathInProject", JsonPrimitive(testJavaFile.name))
put("oldText", JsonPrimitive("content"))
put("newText", JsonPrimitive("updated content"))
},
"ok"
)
}
@Test
fun replace_file_text_by_path() = runBlocking {
testMcpTool(
TextToolset::replace_file_text_by_path.name,
buildJsonObject {
put("pathInProject", JsonPrimitive(mainJavaFile.name))
put("text", JsonPrimitive("updated content"))
},
"ok"
)
}
}

View File

@@ -0,0 +1,35 @@
@file:Suppress("TestFunctionName")
package com.intellij.mcpserver.toolsets
import com.intellij.mcpserver.McpToolsetTestBase
import com.intellij.mcpserver.toolsets.vcs.VcsToolset
import io.kotest.common.runBlocking
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
class VcsToolsetTest : McpToolsetTestBase() {
@Disabled("Configure VCS in test project")
@Test
fun get_project_vcs_status() = runBlocking {
testMcpTool(
VcsToolset::get_project_vcs_status.name,
buildJsonObject {},
"[]"
)
}
@Disabled("Configure VCS in test project")
@Test
fun find_commit_by_message() = runBlocking {
testMcpTool(
VcsToolset::find_commit_by_message.name,
buildJsonObject {
put("text", JsonPrimitive("test commit"))
},
"Error: No VCS configured for this project"
)
}
}