Skip to content

Commit 25364df

Browse files
authored
Merge branch 'main' into devcrocod/update-samples
2 parents 51b814d + 20e4e44 commit 25364df

File tree

4 files changed

+95
-77
lines changed

4 files changed

+95
-77
lines changed

kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import io.modelcontextprotocol.kotlin.sdk.ReadResourceRequest
2121
import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult
2222
import io.modelcontextprotocol.kotlin.sdk.Resource
2323
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
24+
import io.modelcontextprotocol.kotlin.sdk.TextContent
2425
import io.modelcontextprotocol.kotlin.sdk.Tool
2526
import io.modelcontextprotocol.kotlin.sdk.ToolAnnotations
2627
import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions
@@ -32,6 +33,7 @@ import kotlinx.collections.immutable.minus
3233
import kotlinx.collections.immutable.persistentListOf
3334
import kotlinx.collections.immutable.persistentMapOf
3435
import kotlinx.collections.immutable.toPersistentSet
36+
import kotlinx.coroutines.CancellationException
3537

3638
private val logger = KotlinLogging.logger {}
3739

@@ -515,13 +517,30 @@ public open class Server(
515517

516518
private suspend fun handleCallTool(request: CallToolRequest): CallToolResult {
517519
logger.debug { "Handling tool call request for tool: ${request.name}" }
520+
521+
// Check if tool exists
518522
val tool = _tools.value[request.name]
519523
?: run {
520524
logger.error { "Tool not found: ${request.name}" }
521-
throw IllegalArgumentException("Tool not found: ${request.name}")
525+
return CallToolResult(
526+
content = listOf(TextContent(text = "Tool ${request.name} not found")),
527+
isError = true,
528+
)
522529
}
523-
logger.trace { "Executing tool ${request.name} with input: ${request.arguments}" }
524-
return tool.handler(request)
530+
531+
// Execute tool handler and catch any errors
532+
return try {
533+
logger.trace { "Executing tool ${request.name} with input: ${request.arguments}" }
534+
tool.handler(request)
535+
} catch (e: CancellationException) {
536+
throw e
537+
} catch (e: Exception) {
538+
logger.error(e) { "Error executing tool ${request.name}" }
539+
CallToolResult(
540+
content = listOf(TextContent(text = "Error executing tool ${request.name}: ${e.message}")),
541+
isError = true,
542+
)
543+
}
525544
}
526545

527546
private suspend fun handleListPrompts(): ListPromptsResult {

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import kotlinx.serialization.json.buildJsonArray
2121
import kotlinx.serialization.json.buildJsonObject
2222
import kotlinx.serialization.json.put
2323
import org.junit.jupiter.api.Test
24-
import org.junit.jupiter.api.assertThrows
2524
import java.text.DecimalFormat
2625
import java.text.DecimalFormatSymbols
2726
import java.util.Locale
@@ -527,16 +526,18 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() {
527526
"message" to "My exception message",
528527
)
529528

530-
val exception = assertThrows<IllegalStateException> {
531-
runBlocking {
532-
client.callTool(errorToolName, exceptionArgs)
533-
}
534-
}
529+
val exceptionResult = client.callTool(errorToolName, exceptionArgs) as CallToolResultBase
535530

536-
val msg = exception.message ?: ""
537-
val expectedMessage = "JSONRPCError(code=InternalError, message=My exception message, data={})"
531+
assertTrue(exceptionResult.isError ?: false, "isError should be true for exception")
538532

539-
assertEquals(expectedMessage, msg, "Unexpected error message for exception")
533+
val exceptionContent = exceptionResult.content.firstOrNull { it is TextContent } as? TextContent
534+
assertNotNull(exceptionContent, "Error content should be present in the result")
535+
536+
val exceptionText = exceptionContent.text ?: ""
537+
assertTrue(
538+
exceptionText.contains("Error executing tool") && exceptionText.contains("My exception message"),
539+
"Error message should contain the exception details",
540+
)
540541
}
541542

542543
@Test
@@ -770,15 +771,21 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() {
770771
val nonExistentToolName = "non-existent-tool"
771772
val arguments = mapOf("text" to "Test")
772773

773-
val exception = assertThrows<IllegalStateException> {
774-
runBlocking {
775-
client.callTool(nonExistentToolName, arguments)
776-
}
774+
val result = runBlocking {
775+
client.callTool(nonExistentToolName, arguments)
777776
}
778777

779-
val msg = exception.message ?: ""
780-
val expectedMessage = "JSONRPCError(code=InternalError, message=Tool not found: non-existent-tool, data={})"
778+
assertNotNull(result, "Tool call result should not be null")
779+
val callResult = result as CallToolResult
780+
assertTrue(callResult.isError ?: false, "isError should be true for non-existent tool")
781781

782-
assertEquals(expectedMessage, msg, "Unexpected error message for non-existent tool")
782+
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
783+
assertNotNull(textContent, "Error content should be present in the result")
784+
785+
val errorText = textContent.text ?: ""
786+
assertTrue(
787+
errorText.contains("non-existent-tool") && errorText.contains("not found"),
788+
"Error message should indicate the tool was not found",
789+
)
783790
}
784791
}

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import org.junit.jupiter.api.AfterEach
1717
import org.junit.jupiter.api.BeforeEach
1818
import org.junit.jupiter.api.Test
1919
import org.junit.jupiter.api.Timeout
20-
import org.junit.jupiter.api.assertThrows
2120
import java.util.concurrent.TimeUnit
2221
import kotlin.test.assertEquals
2322
import kotlin.test.assertNotNull
@@ -74,16 +73,19 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() {
7473
val nonExistentToolName = "non-existent-tool"
7574
val arguments = mapOf("name" to "TestUser")
7675

77-
val exception = assertThrows<IllegalStateException> {
78-
client.callTool(nonExistentToolName, arguments)
79-
}
76+
val result = client.callTool(nonExistentToolName, arguments)
77+
assertNotNull(result, "Tool call result should not be null")
78+
79+
val callResult = result as CallToolResult
80+
assertTrue(callResult.isError ?: false, "isError should be true for non-existent tool")
8081

81-
val expectedMessage =
82-
"JSONRPCError(code=InvalidParams, message=MCP error -32602: Tool non-existent-tool not found, data={})"
83-
assertEquals(
84-
expectedMessage,
85-
exception.message,
86-
"Unexpected error message for non-existent tool",
82+
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
83+
assertNotNull(textContent, "Error content should be present in the result")
84+
85+
val errorText = textContent.text ?: ""
86+
assertTrue(
87+
errorText.contains("non-existent-tool") && errorText.contains("not found"),
88+
"Error message should indicate the tool was not found",
8789
)
8890
}
8991
}
@@ -176,26 +178,20 @@ class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() {
176178
"name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))),
177179
)
178180

179-
val exception = assertThrows<IllegalStateException> {
180-
client.callTool("greet", invalidArguments)
181-
}
181+
val result = client.callTool("greet", invalidArguments)
182+
assertNotNull(result, "Tool call result should not be null")
183+
184+
val callResult = result as CallToolResult
185+
assertTrue(callResult.isError ?: false, "isError should be true for invalid arguments")
182186

183-
val msg = exception.message ?: ""
184-
val expectedMessage = """
185-
JSONRPCError(code=InvalidParams, message=MCP error -32602: Invalid arguments for tool greet: [
186-
{
187-
"code": "invalid_type",
188-
"expected": "string",
189-
"received": "object",
190-
"path": [
191-
"name"
192-
],
193-
"message": "Expected string, received object"
194-
}
195-
], data={})
196-
""".trimIndent()
197-
198-
assertEquals(expectedMessage, msg, "Unexpected error message for invalid arguments")
187+
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
188+
assertNotNull(textContent, "Error content should be present in the result")
189+
190+
val errorText = textContent.text ?: ""
191+
assertTrue(
192+
errorText.contains("Invalid arguments") && errorText.contains("greet"),
193+
"Error message should indicate invalid arguments for tool greet",
194+
)
199195
}
200196
}
201197

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import kotlinx.serialization.json.JsonObject
1414
import kotlinx.serialization.json.JsonPrimitive
1515
import org.junit.jupiter.api.Test
1616
import org.junit.jupiter.api.Timeout
17-
import org.junit.jupiter.api.assertThrows
1817
import java.util.concurrent.TimeUnit
1918
import kotlin.test.assertEquals
2019
import kotlin.test.assertNotNull
@@ -31,16 +30,19 @@ class KotlinClientTsServerEdgeCasesTestStdio : TsTestBase() {
3130
val nonExistentToolName = "non-existent-tool"
3231
val arguments = mapOf("name" to "TestUser")
3332

34-
val exception = assertThrows<IllegalStateException> {
35-
client.callTool(nonExistentToolName, arguments)
36-
}
33+
val result = client.callTool(nonExistentToolName, arguments)
34+
assertNotNull(result, "Tool call result should not be null")
35+
36+
val callResult = result as CallToolResult
37+
assertTrue(callResult.isError ?: false, "isError should be true for non-existent tool")
3738

38-
val expectedMessage =
39-
"JSONRPCError(code=InvalidParams, message=MCP error -32602: Tool non-existent-tool not found, data={})"
40-
assertEquals(
41-
expectedMessage,
42-
exception.message,
43-
"Unexpected error message for non-existent tool",
39+
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
40+
assertNotNull(textContent, "Error content should be present in the result")
41+
42+
val errorText = textContent.text ?: ""
43+
assertTrue(
44+
errorText.contains("non-existent-tool") && errorText.contains("not found"),
45+
"Error message should indicate the tool was not found",
4446
)
4547
}
4648
}
@@ -133,26 +135,20 @@ class KotlinClientTsServerEdgeCasesTestStdio : TsTestBase() {
133135
"name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))),
134136
)
135137

136-
val exception = assertThrows<IllegalStateException> {
137-
client.callTool("greet", invalidArguments)
138-
}
138+
val result = client.callTool("greet", invalidArguments)
139+
assertNotNull(result, "Tool call result should not be null")
140+
141+
val callResult = result as CallToolResult
142+
assertTrue(callResult.isError ?: false, "isError should be true for invalid arguments")
139143

140-
val msg = exception.message ?: ""
141-
val expectedMessage = """
142-
JSONRPCError(code=InvalidParams, message=MCP error -32602: Invalid arguments for tool greet: [
143-
{
144-
"code": "invalid_type",
145-
"expected": "string",
146-
"received": "object",
147-
"path": [
148-
"name"
149-
],
150-
"message": "Expected string, received object"
151-
}
152-
], data={})
153-
""".trimIndent()
154-
155-
assertEquals(expectedMessage, msg, "Unexpected error message for invalid arguments")
144+
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
145+
assertNotNull(textContent, "Error content should be present in the result")
146+
147+
val errorText = textContent.text ?: ""
148+
assertTrue(
149+
errorText.contains("Invalid arguments") && errorText.contains("greet"),
150+
"Error message should indicate invalid arguments for tool greet",
151+
)
156152
}
157153
}
158154

0 commit comments

Comments
 (0)