Deprecation cycle (#498)
- increase the deprecation level - remove tests that use deprecated objects with deprecation level = error ## Motivation and Context deprecation lifecycle ## How Has This Been Tested? - ## Breaking Changes - ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed
This commit is contained in:
parent
6f1c120c27
commit
92823b987f
54 changed files with 224 additions and 8255 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,123 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.client
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.cio.CIO
|
||||
import io.ktor.server.engine.EmbeddedServer
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.routing.routing
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.Server
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.mcp
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import io.ktor.client.plugins.sse.SSE as ClientSSE
|
||||
import io.ktor.server.sse.SSE as ServerSSE
|
||||
|
||||
class OldSchemaSseTransportTest : BaseTransportTest() {
|
||||
|
||||
private suspend fun EmbeddedServer<*, *>.actualPort() = engine.resolvedConnectors().first().port
|
||||
|
||||
private lateinit var mcpServer: Server
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
mcpServer = Server(
|
||||
serverInfo = Implementation(
|
||||
name = "test-server",
|
||||
version = "1.0",
|
||||
),
|
||||
options = ServerOptions(ServerCapabilities()),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // Ignored because it doesn’t work with wasm/js in Ktor 3.2.3
|
||||
fun `should start then close cleanly`() = runTest {
|
||||
val server = embeddedServer(CIO, port = 0) {
|
||||
install(ServerSSE)
|
||||
routing {
|
||||
mcp { mcpServer }
|
||||
}
|
||||
}.startSuspend(wait = false)
|
||||
|
||||
val actualPort = server.actualPort()
|
||||
|
||||
val transport = HttpClient {
|
||||
install(ClientSSE)
|
||||
}.mcpSseTransport {
|
||||
url {
|
||||
host = "localhost"
|
||||
this.port = actualPort
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
testTransportRead(transport)
|
||||
} finally {
|
||||
server.stopSuspend()
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
fun `should read messages`() = runTest {
|
||||
val server = embeddedServer(CIO, port = 0) {
|
||||
install(ServerSSE)
|
||||
routing {
|
||||
mcp { mcpServer }
|
||||
}
|
||||
}.startSuspend(wait = false)
|
||||
|
||||
val actualPort = server.actualPort()
|
||||
|
||||
val transport = HttpClient {
|
||||
install(ClientSSE)
|
||||
}.mcpSseTransport {
|
||||
url {
|
||||
host = "localhost"
|
||||
this.port = actualPort
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
testTransportRead(transport)
|
||||
} finally {
|
||||
server.stopSuspend()
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
fun `test sse path not root path`() = runTest {
|
||||
val server = embeddedServer(CIO, port = 0) {
|
||||
install(ServerSSE)
|
||||
routing {
|
||||
mcp { mcpServer }
|
||||
}
|
||||
}.startSuspend(wait = false)
|
||||
|
||||
val actualPort = server.actualPort()
|
||||
|
||||
val transport = HttpClient {
|
||||
install(ClientSSE)
|
||||
}.mcpSseTransport {
|
||||
url {
|
||||
host = "localhost"
|
||||
this.port = actualPort
|
||||
pathSegments = listOf("sse")
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
testTransportRead(transport)
|
||||
} finally {
|
||||
server.stopSuspend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.toJSON
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class OldSchemaInMemoryTransportTest {
|
||||
private lateinit var clientTransport: InMemoryTransport
|
||||
private lateinit var serverTransport: InMemoryTransport
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
val (client, server) = InMemoryTransport.createLinkedPair()
|
||||
clientTransport = client
|
||||
serverTransport = server
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should create linked pair`() {
|
||||
assertNotNull(clientTransport)
|
||||
assertNotNull(serverTransport)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should start without error`() = runTest {
|
||||
clientTransport.start()
|
||||
serverTransport.start()
|
||||
// If no exception is thrown, the test passes
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should send message from client to server`() = runTest {
|
||||
val message = InitializedNotification()
|
||||
|
||||
var receivedMessage: JSONRPCMessage? = null
|
||||
serverTransport.onMessage { msg ->
|
||||
receivedMessage = msg
|
||||
}
|
||||
|
||||
val rpcNotification = message.toJSON()
|
||||
clientTransport.send(rpcNotification)
|
||||
assertEquals(rpcNotification, receivedMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should send message from server to client`() = runTest {
|
||||
val message = InitializedNotification()
|
||||
.toJSON()
|
||||
|
||||
var receivedMessage: JSONRPCMessage? = null
|
||||
clientTransport.onMessage { msg ->
|
||||
receivedMessage = msg
|
||||
}
|
||||
|
||||
serverTransport.send(message)
|
||||
assertEquals(message, receivedMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle close`() = runTest {
|
||||
var clientClosed = false
|
||||
var serverClosed = false
|
||||
|
||||
clientTransport.onClose {
|
||||
clientClosed = true
|
||||
}
|
||||
|
||||
serverTransport.onClose {
|
||||
serverClosed = true
|
||||
}
|
||||
|
||||
clientTransport.close()
|
||||
assertTrue(clientClosed)
|
||||
assertTrue(serverClosed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should throw error when sending after close`() = runTest {
|
||||
clientTransport.close()
|
||||
|
||||
assertFailsWith<IllegalStateException> {
|
||||
clientTransport.send(
|
||||
InitializedNotification().toJSON(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should queue messages sent before start`() = runTest {
|
||||
val message = InitializedNotification()
|
||||
.toJSON()
|
||||
|
||||
var receivedMessage: JSONRPCMessage? = null
|
||||
serverTransport.onMessage { msg ->
|
||||
receivedMessage = msg
|
||||
}
|
||||
|
||||
clientTransport.send(message)
|
||||
serverTransport.start()
|
||||
assertEquals(message, receivedMessage)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.shared
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.PingRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.toJSON
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
abstract class OldSchemaBaseTransportTest {
|
||||
|
||||
protected suspend fun testTransportOpenClose(transport: Transport) {
|
||||
transport.onError { error ->
|
||||
fail("Unexpected error: $error")
|
||||
}
|
||||
|
||||
var didClose = false
|
||||
transport.onClose { didClose = true }
|
||||
|
||||
transport.start()
|
||||
assertFalse(didClose, "Transport should not be closed immediately after start")
|
||||
|
||||
transport.close()
|
||||
assertTrue(didClose, "Transport should be closed after close() call")
|
||||
}
|
||||
|
||||
protected suspend fun testTransportRead(transport: Transport) {
|
||||
transport.onError { error ->
|
||||
error.printStackTrace()
|
||||
fail("Unexpected error: $error")
|
||||
}
|
||||
|
||||
val messages = listOf<JSONRPCMessage>(
|
||||
PingRequest().toJSON(),
|
||||
InitializedNotification().toJSON(),
|
||||
)
|
||||
|
||||
val readMessages = mutableListOf<JSONRPCMessage>()
|
||||
val finished = CompletableDeferred<Unit>()
|
||||
|
||||
transport.onMessage { message ->
|
||||
readMessages.add(message)
|
||||
if (message == messages.last()) {
|
||||
finished.complete(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
transport.start()
|
||||
|
||||
for (message in messages) {
|
||||
transport.send(message)
|
||||
}
|
||||
|
||||
finished.await()
|
||||
|
||||
assertEquals(messages, readMessages, "Assert messages received")
|
||||
|
||||
transport.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.shared
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
|
||||
/**
|
||||
* In-memory transport for creating clients and servers that talk to each other within the same process.
|
||||
*/
|
||||
class OldSchemaInMemoryTransport : AbstractTransport() {
|
||||
private var otherTransport: OldSchemaInMemoryTransport? = null
|
||||
private val messageQueue: MutableList<JSONRPCMessage> = mutableListOf()
|
||||
|
||||
/**
|
||||
* Creates a pair of linked in-memory transports that can communicate with each other.
|
||||
* One should be passed to a Client and one to a Server.
|
||||
*/
|
||||
companion object {
|
||||
fun createLinkedPair(): Pair<OldSchemaInMemoryTransport, OldSchemaInMemoryTransport> {
|
||||
val clientTransport = OldSchemaInMemoryTransport()
|
||||
val serverTransport = OldSchemaInMemoryTransport()
|
||||
clientTransport.otherTransport = serverTransport
|
||||
serverTransport.otherTransport = clientTransport
|
||||
return Pair(clientTransport, serverTransport)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun start() {
|
||||
// Process any messages that were queued before start was called
|
||||
while (messageQueue.isNotEmpty()) {
|
||||
messageQueue.removeFirstOrNull()?.let { message ->
|
||||
_onMessage.invoke(message) // todo?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun close() {
|
||||
val other = otherTransport
|
||||
otherTransport = null
|
||||
other?.close()
|
||||
_onClose.invoke()
|
||||
}
|
||||
|
||||
override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) {
|
||||
val other = otherTransport ?: throw IllegalStateException("Not connected")
|
||||
|
||||
other._onMessage.invoke(message)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,698 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptArgument
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.Role
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.McpException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
abstract class OldSchemaAbstractPromptIntegrationTest : OldSchemaKotlinTestBase() {
|
||||
|
||||
private val basicPromptName = "basic-prompt"
|
||||
private val basicPromptDescription = "A basic prompt for testing"
|
||||
|
||||
private val complexPromptName = "multimodal-prompt"
|
||||
private val complexPromptDescription = "A prompt with multiple content types"
|
||||
private val conversationPromptName = "conversation"
|
||||
private val conversationPromptDescription = "A prompt with multiple messages and roles"
|
||||
private val strictPromptName = "strict-prompt"
|
||||
private val strictPromptDescription = "A prompt with required arguments"
|
||||
|
||||
private val largePromptName = "large-prompt"
|
||||
private val largePromptDescription = "A very large prompt for testing"
|
||||
private val largePromptContent = "X".repeat(100_000) // 100KB of data
|
||||
|
||||
private val specialCharsPromptName = "special-chars-prompt"
|
||||
private val specialCharsPromptDescription = "A prompt with special characters"
|
||||
private val specialCharsContent = "!@#$%^&*()_+{}|:\"<>?~`-=[]\\;',./\n\t"
|
||||
|
||||
override fun configureServerCapabilities(): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities =
|
||||
ServerCapabilities(
|
||||
prompts = ServerCapabilities.Prompts(
|
||||
listChanged = true,
|
||||
),
|
||||
)
|
||||
|
||||
override fun configureServer() {
|
||||
// basic prompt with a name parameter
|
||||
server.addPrompt(
|
||||
name = basicPromptName,
|
||||
description = basicPromptDescription,
|
||||
arguments = listOf(
|
||||
PromptArgument(
|
||||
name = "name",
|
||||
description = "The name to greet",
|
||||
required = true,
|
||||
),
|
||||
),
|
||||
) { request ->
|
||||
val name = request.params.arguments?.get("name") ?: "World"
|
||||
|
||||
GetPromptResult(
|
||||
description = basicPromptDescription,
|
||||
messages = listOf(
|
||||
PromptMessage(
|
||||
role = Role.user,
|
||||
content = TextContent(text = "Hello, $name!"),
|
||||
),
|
||||
PromptMessage(
|
||||
role = Role.assistant,
|
||||
content = TextContent(text = "Greetings, $name! How can I assist you today?"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// special chars prompt
|
||||
server.addPrompt(
|
||||
name = specialCharsPromptName,
|
||||
description = specialCharsPromptDescription,
|
||||
arguments = listOf(
|
||||
PromptArgument(
|
||||
name = "special",
|
||||
description = "Special characters to include",
|
||||
required = false,
|
||||
),
|
||||
),
|
||||
) { request ->
|
||||
val special = request.params.arguments?.get("special") ?: specialCharsContent
|
||||
|
||||
GetPromptResult(
|
||||
description = specialCharsPromptDescription,
|
||||
messages = listOf(
|
||||
PromptMessage(
|
||||
role = Role.user,
|
||||
content = TextContent(text = "Special characters: $special"),
|
||||
),
|
||||
PromptMessage(
|
||||
role = Role.assistant,
|
||||
content = TextContent(text = "Received special characters: $special"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// very large prompt
|
||||
server.addPrompt(
|
||||
name = largePromptName,
|
||||
description = largePromptDescription,
|
||||
arguments = listOf(
|
||||
PromptArgument(
|
||||
name = "size",
|
||||
description = "Size multiplier",
|
||||
required = false,
|
||||
),
|
||||
),
|
||||
) { request ->
|
||||
val size = request.params.arguments?.get("size")?.toIntOrNull() ?: 1
|
||||
val content = largePromptContent.repeat(size)
|
||||
|
||||
GetPromptResult(
|
||||
description = largePromptDescription,
|
||||
messages = listOf(
|
||||
PromptMessage(
|
||||
role = Role.user,
|
||||
content = TextContent(text = "Generate a large response"),
|
||||
),
|
||||
PromptMessage(
|
||||
role = Role.assistant,
|
||||
content = TextContent(text = content),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// complext prompt
|
||||
server.addPrompt(
|
||||
name = complexPromptName,
|
||||
description = complexPromptDescription,
|
||||
arguments = listOf(
|
||||
PromptArgument(name = "arg1", description = "Argument 1", required = true),
|
||||
PromptArgument(name = "arg2", description = "Argument 2", required = true),
|
||||
PromptArgument(name = "arg3", description = "Argument 3", required = true),
|
||||
PromptArgument(name = "arg4", description = "Argument 4", required = false),
|
||||
PromptArgument(name = "arg5", description = "Argument 5", required = false),
|
||||
PromptArgument(name = "arg6", description = "Argument 6", required = false),
|
||||
PromptArgument(name = "arg7", description = "Argument 7", required = false),
|
||||
PromptArgument(name = "arg8", description = "Argument 8", required = false),
|
||||
PromptArgument(name = "arg9", description = "Argument 9", required = false),
|
||||
PromptArgument(name = "arg10", description = "Argument 10", required = false),
|
||||
),
|
||||
) { request ->
|
||||
// validate required arguments
|
||||
val requiredArgs = listOf("arg1", "arg2", "arg3")
|
||||
for (argName in requiredArgs) {
|
||||
if (request.params.arguments?.get(argName) == null) {
|
||||
throw IllegalArgumentException("Missing required argument: $argName")
|
||||
}
|
||||
}
|
||||
|
||||
val args = mutableMapOf<String, String>()
|
||||
for (i in 1..10) {
|
||||
val argName = "arg$i"
|
||||
val argValue = request.params.arguments?.get(argName)
|
||||
if (argValue != null) {
|
||||
args[argName] = argValue
|
||||
}
|
||||
}
|
||||
|
||||
GetPromptResult(
|
||||
description = complexPromptDescription,
|
||||
messages = listOf(
|
||||
PromptMessage(
|
||||
role = Role.user,
|
||||
content = TextContent(
|
||||
text = "Arguments: ${
|
||||
args.entries.joinToString {
|
||||
"${it.key}=${it.value}"
|
||||
}
|
||||
}",
|
||||
),
|
||||
),
|
||||
PromptMessage(
|
||||
role = Role.assistant,
|
||||
content = TextContent(text = "Received ${args.size} arguments"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// prompt with multiple messages and roles
|
||||
server.addPrompt(
|
||||
name = conversationPromptName,
|
||||
description = conversationPromptDescription,
|
||||
arguments = listOf(
|
||||
PromptArgument(
|
||||
name = "topic",
|
||||
description = "The topic of the conversation",
|
||||
required = false,
|
||||
),
|
||||
),
|
||||
) { request ->
|
||||
val topic = request.params.arguments?.get("topic") ?: "weather"
|
||||
|
||||
GetPromptResult(
|
||||
description = conversationPromptDescription,
|
||||
messages = listOf(
|
||||
PromptMessage(
|
||||
role = Role.user,
|
||||
content = TextContent(text = "Let's talk about the $topic."),
|
||||
),
|
||||
PromptMessage(
|
||||
role = Role.assistant,
|
||||
content = TextContent(
|
||||
text = "Sure, I'd love to discuss the $topic. What would you like to know?",
|
||||
),
|
||||
),
|
||||
PromptMessage(
|
||||
role = Role.user,
|
||||
content = TextContent(text = "What's your opinion on the $topic?"),
|
||||
),
|
||||
PromptMessage(
|
||||
role = Role.assistant,
|
||||
content = TextContent(
|
||||
text = "As an AI, I don't have personal opinions," +
|
||||
" but I can provide information about $topic.",
|
||||
),
|
||||
),
|
||||
PromptMessage(
|
||||
role = Role.user,
|
||||
content = TextContent(text = "That's helpful, thank you!"),
|
||||
),
|
||||
PromptMessage(
|
||||
role = Role.assistant,
|
||||
content = TextContent(
|
||||
text = "You're welcome! Let me know if you have more questions about $topic.",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// prompt with strict required arguments
|
||||
server.addPrompt(
|
||||
name = strictPromptName,
|
||||
description = strictPromptDescription,
|
||||
arguments = listOf(
|
||||
PromptArgument(
|
||||
name = "requiredArg1",
|
||||
description = "First required argument",
|
||||
required = true,
|
||||
),
|
||||
PromptArgument(
|
||||
name = "requiredArg2",
|
||||
description = "Second required argument",
|
||||
required = true,
|
||||
),
|
||||
PromptArgument(
|
||||
name = "optionalArg",
|
||||
description = "Optional argument",
|
||||
required = false,
|
||||
),
|
||||
),
|
||||
) { request ->
|
||||
val args = request.params.arguments ?: emptyMap()
|
||||
val arg1 = args["requiredArg1"] ?: throw IllegalArgumentException(
|
||||
"Missing required argument: requiredArg1",
|
||||
)
|
||||
val arg2 = args["requiredArg2"] ?: throw IllegalArgumentException(
|
||||
"Missing required argument: requiredArg2",
|
||||
)
|
||||
val optArg = args["optionalArg"] ?: "default"
|
||||
|
||||
GetPromptResult(
|
||||
description = strictPromptDescription,
|
||||
messages = listOf(
|
||||
PromptMessage(
|
||||
role = Role.user,
|
||||
content = TextContent(text = "Required arguments: $arg1, $arg2. Optional: $optArg"),
|
||||
),
|
||||
PromptMessage(
|
||||
role = Role.assistant,
|
||||
content = TextContent(text = "I received your arguments: $arg1, $arg2, and $optArg"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testListPrompts() = runBlocking(Dispatchers.IO) {
|
||||
val result = client.listPrompts()
|
||||
|
||||
assertNotNull(result, "List prompts result should not be null")
|
||||
assertTrue(result.prompts.isNotEmpty(), "Prompts list should not be empty")
|
||||
|
||||
val testPrompt = result.prompts.find { it.name == basicPromptName }
|
||||
assertNotNull(testPrompt, "Test prompt should be in the list")
|
||||
assertEquals(
|
||||
basicPromptDescription,
|
||||
testPrompt.description,
|
||||
"Prompt description should match",
|
||||
)
|
||||
|
||||
val arguments = testPrompt.arguments ?: error("Prompt arguments should not be null")
|
||||
assertTrue(arguments.isNotEmpty(), "Prompt arguments should not be empty")
|
||||
|
||||
val nameArg = arguments.find { it.name == "name" }
|
||||
assertNotNull(nameArg, "Name argument should be in the list")
|
||||
assertEquals(
|
||||
"The name to greet",
|
||||
nameArg.description,
|
||||
"Argument description should match",
|
||||
)
|
||||
assertEquals(true, nameArg.required, "Argument required flag should match")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetPrompt() = runBlocking(Dispatchers.IO) {
|
||||
val testName = "Alice"
|
||||
val result = client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
name = basicPromptName,
|
||||
arguments = mapOf("name" to testName),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertNotNull(result, "Get prompt result should not be null")
|
||||
assertEquals(
|
||||
basicPromptDescription,
|
||||
result.description,
|
||||
"Prompt description should match",
|
||||
)
|
||||
|
||||
assertTrue(result.messages.isNotEmpty(), "Prompt messages should not be empty")
|
||||
assertEquals(2, result.messages.size, "Prompt should have 2 messages")
|
||||
|
||||
val userMessage = result.messages.find { it.role == Role.user }
|
||||
assertNotNull(userMessage, "User message should be in the list")
|
||||
val userContent = userMessage.content as? TextContent
|
||||
assertNotNull(userContent, "User message content should be TextContent")
|
||||
assertNotNull(userContent.text, "User message text should not be null")
|
||||
assertEquals(
|
||||
"Hello, $testName!",
|
||||
userContent.text,
|
||||
"User message content should match",
|
||||
)
|
||||
|
||||
val assistantMessage = result.messages.find { it.role == Role.assistant }
|
||||
assertNotNull(assistantMessage, "Assistant message should be in the list")
|
||||
val assistantContent = assistantMessage.content as? TextContent
|
||||
assertNotNull(assistantContent, "Assistant message content should be TextContent")
|
||||
assertNotNull(assistantContent.text, "Assistant message text should not be null")
|
||||
assertEquals(
|
||||
"Greetings, $testName! How can I assist you today?",
|
||||
assistantContent.text,
|
||||
"Assistant message content should match",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMissingRequiredArguments() = runBlocking(Dispatchers.IO) {
|
||||
val promptsList = client.listPrompts()
|
||||
assertNotNull(promptsList, "Prompts list should not be null")
|
||||
val strictPrompt = promptsList.prompts.find { it.name == strictPromptName }
|
||||
assertNotNull(strictPrompt, "Strict prompt should be in the list")
|
||||
|
||||
val argsDef = strictPrompt.arguments ?: error("Prompt arguments should not be null")
|
||||
val requiredArgs = argsDef.filter { it.required == true }
|
||||
assertEquals(
|
||||
2,
|
||||
requiredArgs.size,
|
||||
"Strict prompt should have 2 required arguments",
|
||||
)
|
||||
|
||||
// test missing required arg
|
||||
val exception = assertThrows<McpException> {
|
||||
runBlocking {
|
||||
client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
name = strictPromptName,
|
||||
arguments = mapOf("requiredArg1" to "value1"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
true,
|
||||
exception.message?.contains("requiredArg2"),
|
||||
"Exception should mention the missing argument",
|
||||
)
|
||||
|
||||
// test with no args
|
||||
val exception2 = assertThrows<McpException> {
|
||||
runBlocking {
|
||||
client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
name = strictPromptName,
|
||||
arguments = emptyMap(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
exception2.message?.contains("requiredArg"),
|
||||
true,
|
||||
"Exception should mention a missing required argument",
|
||||
)
|
||||
|
||||
// test with all required args
|
||||
val result = client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
name = strictPromptName,
|
||||
arguments = mapOf(
|
||||
"requiredArg1" to "value1",
|
||||
"requiredArg2" to "value2",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertNotNull(result, "Get prompt result should not be null")
|
||||
assertEquals(2, result.messages.size, "Prompt should have 2 messages")
|
||||
|
||||
val userMessage = result.messages.find { it.role == Role.user }
|
||||
assertNotNull(userMessage, "User message should be in the list")
|
||||
val userContent = userMessage.content as? TextContent
|
||||
assertNotNull(userContent, "User message content should be TextContent")
|
||||
val userText = requireNotNull(userContent.text)
|
||||
assertTrue(userText.contains("value1"), "Message should contain first argument")
|
||||
assertTrue(userText.contains("value2"), "Message should contain second argument")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleMessagesAndRoles() = runBlocking(Dispatchers.IO) {
|
||||
val topic = "climate change"
|
||||
val result = client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
name = conversationPromptName,
|
||||
arguments = mapOf("topic" to topic),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertNotNull(result, "Get prompt result should not be null")
|
||||
assertEquals(
|
||||
conversationPromptDescription,
|
||||
result.description,
|
||||
"Prompt description should match",
|
||||
)
|
||||
|
||||
assertTrue(result.messages.isNotEmpty(), "Prompt messages should not be empty")
|
||||
assertEquals(6, result.messages.size, "Prompt should have 6 messages")
|
||||
|
||||
val userMessages = result.messages.filter { it.role == Role.user }
|
||||
val assistantMessages = result.messages.filter { it.role == Role.assistant }
|
||||
|
||||
assertEquals(3, userMessages.size, "Should have 3 user messages")
|
||||
assertEquals(3, assistantMessages.size, "Should have 3 assistant messages")
|
||||
|
||||
for (i in 0 until result.messages.size) {
|
||||
val expectedRole = if (i % 2 == 0) Role.user else Role.assistant
|
||||
assertEquals(
|
||||
expectedRole,
|
||||
result.messages[i].role,
|
||||
"Message $i should have role $expectedRole",
|
||||
)
|
||||
}
|
||||
|
||||
for (message in result.messages) {
|
||||
val content = message.content as? TextContent
|
||||
assertNotNull(content, "Message content should be TextContent")
|
||||
val text = requireNotNull(content.text)
|
||||
|
||||
// Either the message contains the topic or it's a generic conversation message
|
||||
val containsTopic = text.contains(topic)
|
||||
val isGenericMessage = text.contains("thank you") || text.contains("welcome")
|
||||
|
||||
assertTrue(
|
||||
containsTopic || isGenericMessage,
|
||||
"Message should either contain the topic or be a generic conversation message",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBasicPrompt() = runBlocking(Dispatchers.IO) {
|
||||
val testName = "Alice"
|
||||
val result = client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
name = basicPromptName,
|
||||
arguments = mapOf("name" to testName),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertNotNull(result, "Get prompt result should not be null")
|
||||
assertEquals(basicPromptDescription, result.description, "Prompt description should match")
|
||||
|
||||
assertEquals(2, result.messages.size, "Prompt should have 2 messages")
|
||||
|
||||
val userMessage = result.messages.find { it.role == Role.user }
|
||||
assertNotNull(userMessage, "User message should be in the list")
|
||||
val userContent = userMessage.content as? TextContent
|
||||
assertNotNull(userContent, "User message content should be TextContent")
|
||||
assertEquals("Hello, $testName!", userContent.text, "User message content should match")
|
||||
|
||||
val assistantMessage = result.messages.find { it.role == Role.assistant }
|
||||
assertNotNull(assistantMessage, "Assistant message should be in the list")
|
||||
val assistantContent = assistantMessage.content as? TextContent
|
||||
assertNotNull(assistantContent, "Assistant message content should be TextContent")
|
||||
assertEquals(
|
||||
"Greetings, $testName! How can I assist you today?",
|
||||
assistantContent.text,
|
||||
"Assistant message content should match",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testComplexPromptWithManyArguments() = runBlocking(Dispatchers.IO) {
|
||||
val arguments = (1..10).associate { i -> "arg$i" to "value$i" }
|
||||
|
||||
val result = client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
name = complexPromptName,
|
||||
arguments = arguments,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertNotNull(result, "Get prompt result should not be null")
|
||||
assertEquals(complexPromptDescription, result.description, "Prompt description should match")
|
||||
|
||||
assertEquals(2, result.messages.size, "Prompt should have 2 messages")
|
||||
|
||||
val userMessage = result.messages.find { it.role == Role.user }
|
||||
assertNotNull(userMessage, "User message should be in the list")
|
||||
val userContent = userMessage.content as? TextContent
|
||||
assertNotNull(userContent, "User message content should be TextContent")
|
||||
|
||||
// verify all arguments
|
||||
val text = userContent.text ?: ""
|
||||
for (i in 1..10) {
|
||||
assertTrue(text.contains("arg$i=value$i"), "Message should contain arg$i=value$i")
|
||||
}
|
||||
|
||||
val assistantMessage = result.messages.find { it.role == Role.assistant }
|
||||
assertNotNull(assistantMessage, "Assistant message should be in the list")
|
||||
val assistantContent = assistantMessage.content as? TextContent
|
||||
assertNotNull(assistantContent, "Assistant message content should be TextContent")
|
||||
assertEquals(
|
||||
"Received 10 arguments",
|
||||
assistantContent.text,
|
||||
"Assistant message should indicate 10 arguments",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLargePrompt() = runBlocking(Dispatchers.IO) {
|
||||
val result = client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
name = largePromptName,
|
||||
arguments = mapOf("size" to "1"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertNotNull(result, "Get prompt result should not be null")
|
||||
assertEquals(largePromptDescription, result.description, "Prompt description should match")
|
||||
|
||||
assertEquals(2, result.messages.size, "Prompt should have 2 messages")
|
||||
|
||||
val assistantMessage = result.messages.find { it.role == Role.assistant }
|
||||
assertNotNull(assistantMessage, "Assistant message should be in the list")
|
||||
val assistantContent = assistantMessage.content as? TextContent
|
||||
assertNotNull(assistantContent, "Assistant message content should be TextContent")
|
||||
val text = assistantContent.text ?: ""
|
||||
assertEquals(100_000, text.length, "Assistant message should be 100KB in size")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSpecialCharacters() = runBlocking(Dispatchers.IO) {
|
||||
val result = client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
name = specialCharsPromptName,
|
||||
arguments = mapOf("special" to specialCharsContent),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertNotNull(result, "Get prompt result should not be null")
|
||||
assertEquals(specialCharsPromptDescription, result.description, "Prompt description should match")
|
||||
|
||||
assertEquals(2, result.messages.size, "Prompt should have 2 messages")
|
||||
|
||||
val userMessage = result.messages.find { it.role == Role.user }
|
||||
assertNotNull(userMessage, "User message should be in the list")
|
||||
val userContent = userMessage.content as? TextContent
|
||||
assertNotNull(userContent, "User message content should be TextContent")
|
||||
val userText = userContent.text ?: ""
|
||||
assertTrue(userText.contains(specialCharsContent), "User message should contain special characters")
|
||||
|
||||
val assistantMessage = result.messages.find { it.role == Role.assistant }
|
||||
assertNotNull(assistantMessage, "Assistant message should be in the list")
|
||||
val assistantContent = assistantMessage.content as? TextContent
|
||||
assertNotNull(assistantContent, "Assistant message content should be TextContent")
|
||||
val assistantText = assistantContent.text ?: ""
|
||||
assertTrue(
|
||||
assistantText.contains(specialCharsContent),
|
||||
"Assistant message should contain special characters",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConcurrentPromptRequests() = runTest {
|
||||
val concurrentCount = 10
|
||||
val results = mutableListOf<GetPromptResult?>()
|
||||
|
||||
runBlocking {
|
||||
repeat(concurrentCount) { index ->
|
||||
launch {
|
||||
val promptName = when (index % 4) {
|
||||
0 -> basicPromptName
|
||||
1 -> complexPromptName
|
||||
2 -> largePromptName
|
||||
else -> specialCharsPromptName
|
||||
}
|
||||
|
||||
val arguments = when (promptName) {
|
||||
basicPromptName -> mapOf("name" to "User$index")
|
||||
complexPromptName -> mapOf("arg1" to "v1", "arg2" to "v2", "arg3" to "v3")
|
||||
largePromptName -> mapOf("size" to "1")
|
||||
else -> mapOf("special" to "!@#$%^&*()")
|
||||
}
|
||||
|
||||
val result = client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
name = promptName,
|
||||
arguments = arguments,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
synchronized(results) {
|
||||
results.add(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(concurrentCount, results.size, "All concurrent operations should complete")
|
||||
|
||||
results.forEach { result ->
|
||||
assertNotNull(result, "Result should not be null")
|
||||
assertTrue(result.messages.isNotEmpty(), "Result messages should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNonExistentPrompt() = runTest {
|
||||
val nonExistentPromptName = "non-existent-prompt"
|
||||
|
||||
val exception = assertThrows<McpException> {
|
||||
runBlocking {
|
||||
client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
name = nonExistentPromptName,
|
||||
arguments = mapOf("name" to "Test"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val msg = exception.message ?: ""
|
||||
val expectedMessage = "MCP error -32603: Prompt not found: non-existent-prompt"
|
||||
|
||||
assertEquals(expectedMessage, msg, "Unexpected error message for non-existent prompt")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,319 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.BlobResourceContents
|
||||
import io.modelcontextprotocol.kotlin.sdk.ReadResourceRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.SubscribeRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.TextResourceContents
|
||||
import io.modelcontextprotocol.kotlin.sdk.UnsubscribeRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.McpException
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequestParams
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequestParams
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertNull
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
abstract class OldSchemaAbstractResourceIntegrationTest : OldSchemaKotlinTestBase() {
|
||||
|
||||
private val testResourceUri = "test://example.txt"
|
||||
private val testResourceName = "Test Resource"
|
||||
private val testResourceDescription = "A test resource for integration testing"
|
||||
private val testResourceContent = "This is the content of the test resource."
|
||||
|
||||
private val binaryResourceUri = "test://image.png"
|
||||
private val binaryResourceName = "Binary Resource"
|
||||
private val binaryResourceDescription = "A binary resource for testing"
|
||||
private val binaryResourceContent =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
|
||||
|
||||
private val largeResourceUri = "test://large.txt"
|
||||
private val largeResourceName = "Large Resource"
|
||||
private val largeResourceDescription = "A large text resource for testing"
|
||||
private val largeResourceContent = "X".repeat(100_000) // 100KB of data
|
||||
|
||||
private val dynamicResourceUri = "test://dynamic.txt"
|
||||
private val dynamicResourceName = "Dynamic Resource"
|
||||
private val dynamicResourceContent = AtomicBoolean(false)
|
||||
|
||||
override fun configureServerCapabilities(): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities =
|
||||
ServerCapabilities(
|
||||
resources = ServerCapabilities.Resources(
|
||||
subscribe = true,
|
||||
listChanged = true,
|
||||
),
|
||||
)
|
||||
|
||||
override fun configureServer() {
|
||||
server.addResource(
|
||||
uri = testResourceUri,
|
||||
name = testResourceName,
|
||||
description = testResourceDescription,
|
||||
mimeType = "text/plain",
|
||||
) { request ->
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = testResourceContent,
|
||||
uri = request.params.uri,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
server.addResource(
|
||||
uri = testResourceUri,
|
||||
name = testResourceName,
|
||||
description = testResourceDescription,
|
||||
mimeType = "text/plain",
|
||||
) { request ->
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = testResourceContent,
|
||||
uri = request.params.uri,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
server.addResource(
|
||||
uri = binaryResourceUri,
|
||||
name = binaryResourceName,
|
||||
description = binaryResourceDescription,
|
||||
mimeType = "image/png",
|
||||
) { request ->
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
BlobResourceContents(
|
||||
blob = binaryResourceContent,
|
||||
uri = request.params.uri,
|
||||
mimeType = "image/png",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
server.addResource(
|
||||
uri = largeResourceUri,
|
||||
name = largeResourceName,
|
||||
description = largeResourceDescription,
|
||||
mimeType = "text/plain",
|
||||
) { request ->
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = largeResourceContent,
|
||||
uri = request.params.uri,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
server.addResource(
|
||||
uri = dynamicResourceUri,
|
||||
name = dynamicResourceName,
|
||||
description = "A resource that can be updated",
|
||||
mimeType = "text/plain",
|
||||
) { request ->
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = if (dynamicResourceContent.get()) "Updated content" else "Original content",
|
||||
uri = request.params.uri,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testListResources() = runBlocking(Dispatchers.IO) {
|
||||
val result = client.listResources()
|
||||
|
||||
assertNotNull(result, "List resources result should not be null")
|
||||
assertNull(actual = result.meta, "List resources meta should be null")
|
||||
assertTrue(result.resources.isNotEmpty(), "Resources list should not be empty")
|
||||
|
||||
val testResource = result.resources.find { it.uri == testResourceUri }
|
||||
assertNotNull(testResource, "Test resource should be in the list")
|
||||
assertEquals(testResourceName, testResource.name, "Resource name should match")
|
||||
assertEquals(testResourceDescription, testResource.description, "Resource description should match")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReadResource() = runBlocking(Dispatchers.IO) {
|
||||
val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = testResourceUri)))
|
||||
|
||||
assertNotNull(result, "Read resource result should not be null")
|
||||
assertNull(actual = result.meta, "Read resource result meta should be null")
|
||||
assertTrue(result.contents.isNotEmpty(), "Resource contents should not be empty")
|
||||
|
||||
val content = result.contents.firstOrNull() as? TextResourceContents
|
||||
assertNotNull(content, "Resource content should be TextResourceContents")
|
||||
assertEquals(testResourceContent, content.text, "Resource content should match")
|
||||
}
|
||||
|
||||
@Ignore("Blocked by https://github.com/modelcontextprotocol/kotlin-sdk/issues/249")
|
||||
@Test
|
||||
fun testSubscribeAndUnsubscribe() {
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val subscribeResult =
|
||||
client.subscribeResource(SubscribeRequest(SubscribeRequestParams(uri = testResourceUri)))
|
||||
assertNotNull(subscribeResult, "Subscribe result should not be null")
|
||||
|
||||
val unsubscribeResult =
|
||||
client.unsubscribeResource(UnsubscribeRequest(UnsubscribeRequestParams(uri = testResourceUri)))
|
||||
assertNotNull(unsubscribeResult, "Unsubscribe result should not be null")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBinaryResource() = runBlocking(Dispatchers.IO) {
|
||||
val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = binaryResourceUri)))
|
||||
|
||||
assertNotNull(result, "Read resource result should not be null")
|
||||
assertTrue(result.contents.isNotEmpty(), "Resource contents should not be empty")
|
||||
|
||||
val content = result.contents.firstOrNull() as? BlobResourceContents
|
||||
assertNotNull(content, "Resource content should be BlobResourceContents")
|
||||
assertEquals(binaryResourceContent, content.blob, "Binary resource content should match")
|
||||
assertEquals("image/png", content.mimeType, "MIME type should match")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLargeResource() = runBlocking(Dispatchers.IO) {
|
||||
val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = largeResourceUri)))
|
||||
|
||||
assertNotNull(result, "Read resource result should not be null")
|
||||
assertTrue(result.contents.isNotEmpty(), "Resource contents should not be empty")
|
||||
|
||||
val content = result.contents.firstOrNull() as? TextResourceContents
|
||||
assertNotNull(content, "Resource content should be TextResourceContents")
|
||||
assertEquals(100_000, content.text.length, "Large resource content length should match")
|
||||
assertEquals("X".repeat(100_000), content.text, "Large resource content should match")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInvalidResourceUri() = runTest {
|
||||
val invalidUri = "test://nonexistent.txt"
|
||||
|
||||
val exception = assertThrows<McpException> {
|
||||
runBlocking {
|
||||
client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = invalidUri)))
|
||||
}
|
||||
}
|
||||
|
||||
val msg = exception.message ?: ""
|
||||
val expectedMessage =
|
||||
"MCP error -32603: Resource not found: test://nonexistent.txt"
|
||||
|
||||
assertEquals(expectedMessage, msg, "Unexpected error message for invalid resource URI")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDynamicResource() = runBlocking(Dispatchers.IO) {
|
||||
val initialResult = client.readResource(
|
||||
ReadResourceRequest(ReadResourceRequestParams(uri = dynamicResourceUri)),
|
||||
)
|
||||
assertNotNull(initialResult, "Initial read result should not be null")
|
||||
val initialContent = (initialResult.contents.firstOrNull() as? TextResourceContents)?.text
|
||||
assertEquals("Original content", initialContent, "Initial content should match")
|
||||
|
||||
// update resource
|
||||
dynamicResourceContent.set(true)
|
||||
|
||||
val updatedResult = client.readResource(
|
||||
ReadResourceRequest(ReadResourceRequestParams(uri = dynamicResourceUri)),
|
||||
)
|
||||
assertNotNull(updatedResult, "Updated read result should not be null")
|
||||
val updatedContent = (updatedResult.contents.firstOrNull() as? TextResourceContents)?.text
|
||||
assertEquals("Updated content", updatedContent, "Updated content should match")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testResourceAddAndRemove() = runBlocking(Dispatchers.IO) {
|
||||
val initialList = client.listResources()
|
||||
assertNotNull(initialList, "Initial list result should not be null")
|
||||
val initialCount = initialList.resources.size
|
||||
|
||||
val newResourceUri = "test://new-resource.txt"
|
||||
server.addResource(
|
||||
uri = newResourceUri,
|
||||
name = "New Resource",
|
||||
description = "A newly added resource",
|
||||
mimeType = "text/plain",
|
||||
) { request ->
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = "New resource content",
|
||||
uri = request.params.uri,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val updatedList = client.listResources()
|
||||
assertNotNull(updatedList, "Updated list result should not be null")
|
||||
val updatedCount = updatedList.resources.size
|
||||
|
||||
assertEquals(initialCount + 1, updatedCount, "Resource count should increase by 1")
|
||||
val newResource = updatedList.resources.find { it.uri == newResourceUri }
|
||||
assertNotNull(newResource, "New resource should be in the list")
|
||||
|
||||
server.removeResource(newResourceUri)
|
||||
|
||||
val finalList = client.listResources()
|
||||
assertNotNull(finalList, "Final list result should not be null")
|
||||
val finalCount = finalList.resources.size
|
||||
|
||||
assertEquals(initialCount, finalCount, "Resource count should return to initial value")
|
||||
val removedResource = finalList.resources.find { it.uri == newResourceUri }
|
||||
assertEquals(null, removedResource, "Resource should be removed from the list")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConcurrentResourceOperations() = runTest {
|
||||
val concurrentCount = 10
|
||||
val results = mutableListOf<ReadResourceResult?>()
|
||||
|
||||
runBlocking {
|
||||
repeat(concurrentCount) { index ->
|
||||
launch {
|
||||
val uri = when (index % 3) {
|
||||
0 -> testResourceUri
|
||||
1 -> binaryResourceUri
|
||||
else -> largeResourceUri
|
||||
}
|
||||
|
||||
val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = uri)))
|
||||
synchronized(results) {
|
||||
results.add(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(concurrentCount, results.size, "All concurrent operations should complete")
|
||||
results.forEach { result ->
|
||||
assertNotNull(result, "Result should not be null")
|
||||
assertTrue(result.contents.isNotEmpty(), "Result contents should not be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,820 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin
|
||||
|
||||
import io.kotest.assertions.json.shouldEqualJson
|
||||
import io.modelcontextprotocol.kotlin.sdk.CallToolRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.CallToolResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ImageContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptMessageContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.Tool
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertNull
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Locale
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
abstract class OldSchemaAbstractToolIntegrationTest : OldSchemaKotlinTestBase() {
|
||||
private val testToolName = "echo"
|
||||
private val testToolDescription = "A simple echo tool that returns the input text"
|
||||
private val complexToolName = "calculator"
|
||||
private val complexToolDescription = "A calculator tool that performs operations on numbers"
|
||||
private val errorToolName = "error-tool"
|
||||
private val errorToolDescription = "A tool that demonstrates error handling"
|
||||
private val multiContentToolName = "multi-content"
|
||||
private val multiContentToolDescription = "A tool that returns multiple content types"
|
||||
|
||||
private val basicToolName = "basic-tool"
|
||||
private val basicToolDescription = "A basic tool for testing"
|
||||
|
||||
private val largeToolName = "large-tool"
|
||||
private val largeToolDescription = "A tool that returns a large response"
|
||||
private val largeToolContent = "X".repeat(100_000) // 100KB of data
|
||||
|
||||
private val slowToolName = "slow-tool"
|
||||
private val slowToolDescription = "A tool that takes time to respond"
|
||||
|
||||
private val specialCharsToolName = "special-chars-tool"
|
||||
private val specialCharsToolDescription = "A tool that handles special characters"
|
||||
private val specialCharsContent = "!@#$%^&*()_+{}|:\"<>?~`-=[]\\;',./\n\t"
|
||||
|
||||
override fun configureServerCapabilities(): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities =
|
||||
ServerCapabilities(
|
||||
tools = ServerCapabilities.Tools(
|
||||
listChanged = true,
|
||||
),
|
||||
)
|
||||
|
||||
override fun configureServer() {
|
||||
setupEchoTool()
|
||||
setupCalculatorTool()
|
||||
setupErrorHandlingTool()
|
||||
setupMultiContentTool()
|
||||
}
|
||||
|
||||
private fun setupEchoTool() {
|
||||
server.addTool(
|
||||
name = testToolName,
|
||||
description = testToolDescription,
|
||||
inputSchema = Tool.Input(
|
||||
properties = buildJsonObject {
|
||||
put(
|
||||
"text",
|
||||
buildJsonObject {
|
||||
put("type", "string")
|
||||
put("description", "The text to echo back")
|
||||
},
|
||||
)
|
||||
},
|
||||
required = listOf("text"),
|
||||
),
|
||||
) { request ->
|
||||
val text = (request.params.arguments?.get("text") as? JsonPrimitive)?.content ?: "No text provided"
|
||||
|
||||
CallToolResult(
|
||||
content = listOf(TextContent(text = "Echo: $text")),
|
||||
structuredContent = buildJsonObject {
|
||||
put("result", text)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCalculatorTool() {
|
||||
server.addTool(
|
||||
name = basicToolName,
|
||||
description = basicToolDescription,
|
||||
inputSchema = Tool.Input(
|
||||
properties = buildJsonObject {
|
||||
put(
|
||||
"text",
|
||||
buildJsonObject {
|
||||
put("type", "string")
|
||||
put("description", "The text to echo back")
|
||||
},
|
||||
)
|
||||
},
|
||||
required = listOf("text"),
|
||||
),
|
||||
) { request ->
|
||||
val text = (request.params.arguments?.get("text") as? JsonPrimitive)?.content ?: "No text provided"
|
||||
|
||||
CallToolResult(
|
||||
content = listOf(TextContent(text = "Echo: $text")),
|
||||
structuredContent = buildJsonObject {
|
||||
put("result", text)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
server.addTool(
|
||||
name = specialCharsToolName,
|
||||
description = specialCharsToolDescription,
|
||||
inputSchema = Tool.Input(
|
||||
properties = buildJsonObject {
|
||||
put(
|
||||
"special",
|
||||
buildJsonObject {
|
||||
put("type", "string")
|
||||
put("description", "Special characters to process")
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
) { request ->
|
||||
val special = (request.params.arguments?.get("special") as? JsonPrimitive)?.content ?: specialCharsContent
|
||||
|
||||
CallToolResult(
|
||||
content = listOf(TextContent(text = "Received special characters: $special")),
|
||||
structuredContent = buildJsonObject {
|
||||
put("special", special)
|
||||
put("length", special.length)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
server.addTool(
|
||||
name = slowToolName,
|
||||
description = slowToolDescription,
|
||||
inputSchema = Tool.Input(
|
||||
properties = buildJsonObject {
|
||||
put(
|
||||
"delay",
|
||||
buildJsonObject {
|
||||
put("type", "integer")
|
||||
put("description", "Delay in milliseconds")
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
) { request ->
|
||||
val delay = (request.params.arguments?.get("delay") as? JsonPrimitive)?.content?.toIntOrNull() ?: 1000
|
||||
|
||||
// simulate slow operation
|
||||
runBlocking {
|
||||
delay(delay.toLong())
|
||||
}
|
||||
|
||||
CallToolResult(
|
||||
content = listOf(TextContent(text = "Completed after ${delay}ms delay")),
|
||||
structuredContent = buildJsonObject {
|
||||
put("delay", delay)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
server.addTool(
|
||||
name = largeToolName,
|
||||
description = largeToolDescription,
|
||||
inputSchema = Tool.Input(
|
||||
properties = buildJsonObject {
|
||||
put(
|
||||
"size",
|
||||
buildJsonObject {
|
||||
put("type", "integer")
|
||||
put("description", "Size multiplier")
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
) { request ->
|
||||
val size = (request.params.arguments?.get("size") as? JsonPrimitive)?.content?.toIntOrNull() ?: 1
|
||||
val content = largeToolContent.take(largeToolContent.length.coerceAtMost(size * 1000))
|
||||
|
||||
CallToolResult(
|
||||
content = listOf(TextContent(text = content)),
|
||||
structuredContent = buildJsonObject {
|
||||
put("size", content.length)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
server.addTool(
|
||||
name = complexToolName,
|
||||
description = complexToolDescription,
|
||||
inputSchema = Tool.Input(
|
||||
properties = buildJsonObject {
|
||||
put(
|
||||
"operation",
|
||||
buildJsonObject {
|
||||
put("type", "string")
|
||||
put("description", "The operation to perform (add, subtract, multiply, divide)")
|
||||
put(
|
||||
"enum",
|
||||
buildJsonArray {
|
||||
add("add")
|
||||
add("subtract")
|
||||
add("multiply")
|
||||
add("divide")
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
put(
|
||||
"a",
|
||||
buildJsonObject {
|
||||
put("type", "number")
|
||||
put("description", "First operand")
|
||||
},
|
||||
)
|
||||
put(
|
||||
"b",
|
||||
buildJsonObject {
|
||||
put("type", "number")
|
||||
put("description", "Second operand")
|
||||
},
|
||||
)
|
||||
put(
|
||||
"precision",
|
||||
buildJsonObject {
|
||||
put("type", "integer")
|
||||
put("description", "Number of decimal places (optional)")
|
||||
put("default", 2)
|
||||
},
|
||||
)
|
||||
put(
|
||||
"showSteps",
|
||||
buildJsonObject {
|
||||
put("type", "boolean")
|
||||
put("description", "Whether to show calculation steps")
|
||||
put("default", false)
|
||||
},
|
||||
)
|
||||
put(
|
||||
"tags",
|
||||
buildJsonObject {
|
||||
put("type", "array")
|
||||
put("description", "Optional tags for the calculation")
|
||||
put(
|
||||
"items",
|
||||
buildJsonObject {
|
||||
put("type", "string")
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
required = listOf("operation", "a", "b"),
|
||||
),
|
||||
) { request ->
|
||||
val operation = (request.params.arguments?.get("operation") as? JsonPrimitive)?.content ?: "add"
|
||||
val a = (request.params.arguments?.get("a") as? JsonPrimitive)?.content?.toDoubleOrNull() ?: 0.0
|
||||
val b = (request.params.arguments?.get("b") as? JsonPrimitive)?.content?.toDoubleOrNull() ?: 0.0
|
||||
val precision = (request.params.arguments?.get("precision") as? JsonPrimitive)?.content?.toIntOrNull() ?: 2
|
||||
val showSteps =
|
||||
(request.params.arguments?.get("showSteps") as? JsonPrimitive)?.content?.toBoolean() ?: false
|
||||
val tags = (request.params.arguments?.get("tags") as? JsonArray)?.mapNotNull {
|
||||
(it as? JsonPrimitive)?.content
|
||||
} ?: emptyList()
|
||||
|
||||
val result = when (operation) {
|
||||
"add" -> a + b
|
||||
"subtract" -> a - b
|
||||
"multiply" -> a * b
|
||||
"divide" -> if (b != 0.0) a / b else Double.POSITIVE_INFINITY
|
||||
else -> 0.0
|
||||
}
|
||||
|
||||
val pattern = if (precision > 0) "0." + "0".repeat(precision) else "0"
|
||||
val symbols = DecimalFormatSymbols(Locale.US).apply { decimalSeparator = '.' }
|
||||
val df = DecimalFormat(pattern, symbols).apply { isGroupingUsed = false }
|
||||
val formattedResult = df.format(result)
|
||||
|
||||
val textContent = if (showSteps) {
|
||||
"Operation: $operation\nA: $a\nB: $b\nResult: $formattedResult\nTags: ${
|
||||
tags.joinToString(", ")
|
||||
}"
|
||||
} else {
|
||||
"Result: $formattedResult"
|
||||
}
|
||||
|
||||
CallToolResult(
|
||||
content = listOf(TextContent(text = textContent)),
|
||||
structuredContent = buildJsonObject {
|
||||
put("operation", operation)
|
||||
put("a", a)
|
||||
put("b", b)
|
||||
put("result", result)
|
||||
put("formattedResult", formattedResult)
|
||||
put("precision", precision)
|
||||
put("tags", buildJsonArray { tags.forEach { add(it) } })
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupErrorHandlingTool() {
|
||||
server.addTool(
|
||||
name = errorToolName,
|
||||
description = errorToolDescription,
|
||||
inputSchema = Tool.Input(
|
||||
properties = buildJsonObject {
|
||||
put(
|
||||
"errorType",
|
||||
buildJsonObject {
|
||||
put("type", "string")
|
||||
put("description", "Type of error to simulate (none, exception, error)")
|
||||
put(
|
||||
"enum",
|
||||
buildJsonArray {
|
||||
add("none")
|
||||
add("exception")
|
||||
add("error")
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
put(
|
||||
"message",
|
||||
buildJsonObject {
|
||||
put("type", "string")
|
||||
put("description", "Custom error message")
|
||||
put("default", "An error occurred")
|
||||
},
|
||||
)
|
||||
},
|
||||
required = listOf("errorType"),
|
||||
),
|
||||
) { request ->
|
||||
val errorType = (request.params.arguments?.get("errorType") as? JsonPrimitive)?.content ?: "none"
|
||||
val message = (request.params.arguments?.get("message") as? JsonPrimitive)?.content ?: "An error occurred"
|
||||
|
||||
when (errorType) {
|
||||
"exception" -> throw IllegalArgumentException(message)
|
||||
|
||||
"error" -> CallToolResult(
|
||||
content = listOf(TextContent(text = "Error: $message")),
|
||||
structuredContent = buildJsonObject {
|
||||
put("error", true)
|
||||
put("message", message)
|
||||
},
|
||||
)
|
||||
|
||||
else -> CallToolResult(
|
||||
content = listOf(TextContent(text = "No error occurred")),
|
||||
structuredContent = buildJsonObject {
|
||||
put("error", false)
|
||||
put("message", "Success")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMultiContentTool() {
|
||||
server.addTool(
|
||||
name = multiContentToolName,
|
||||
description = multiContentToolDescription,
|
||||
inputSchema = Tool.Input(
|
||||
properties = buildJsonObject {
|
||||
put(
|
||||
"text",
|
||||
buildJsonObject {
|
||||
put("type", "string")
|
||||
put("description", "Text to include in the response")
|
||||
},
|
||||
)
|
||||
put(
|
||||
"includeImage",
|
||||
buildJsonObject {
|
||||
put("type", "boolean")
|
||||
put("description", "Whether to include an image in the response")
|
||||
put("default", true)
|
||||
},
|
||||
)
|
||||
},
|
||||
required = listOf("text"),
|
||||
),
|
||||
) { request ->
|
||||
val text = (request.params.arguments?.get("text") as? JsonPrimitive)?.content ?: "Default text"
|
||||
val includeImage =
|
||||
(request.params.arguments?.get("includeImage") as? JsonPrimitive)?.content?.toBoolean() ?: true
|
||||
|
||||
val content = mutableListOf<PromptMessageContent>(
|
||||
TextContent(text = "Text content: $text"),
|
||||
)
|
||||
|
||||
if (includeImage) {
|
||||
content.add(
|
||||
ImageContent(
|
||||
data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
|
||||
mimeType = "image/png",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
CallToolResult(
|
||||
content = content,
|
||||
structuredContent = buildJsonObject {
|
||||
put("text", text)
|
||||
put("includeImage", includeImage)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testListTools(): Unit = runBlocking(Dispatchers.IO) {
|
||||
val result = client.listTools()
|
||||
|
||||
assertNotNull(result, "List tools result should not be null")
|
||||
assertNull(actual = result.meta, "List tools result meta should be null")
|
||||
assertTrue(result.tools.isNotEmpty(), "Tools list should not be empty")
|
||||
|
||||
val testTool = result.tools.find { it.name == testToolName }
|
||||
|
||||
assertNotNull(testTool, "Test tool should be in the list")
|
||||
assertEquals(
|
||||
testToolDescription,
|
||||
testTool.description,
|
||||
"Tool description should match",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCallTool(): Unit = runBlocking(Dispatchers.IO) {
|
||||
val testText = "Hello, world!"
|
||||
val arguments = mapOf("text" to testText)
|
||||
|
||||
val result = client.callTool(testToolName, arguments)
|
||||
assertNull(actual = result.meta, "CallTool result meta should be null")
|
||||
|
||||
val actualContent = result.structuredContent.toString()
|
||||
val expectedContent = """
|
||||
{"result":"Hello, world!"}
|
||||
""".trimIndent()
|
||||
|
||||
actualContent shouldEqualJson expectedContent
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testComplexInputSchemaTool(): Unit = runBlocking(Dispatchers.IO) {
|
||||
val toolsList = client.listTools()
|
||||
assertNotNull(toolsList, "Tools list should not be null")
|
||||
val calculatorTool = toolsList.tools.find { it.name == complexToolName }
|
||||
assertNotNull(calculatorTool, "Calculator tool should be in the list")
|
||||
|
||||
val arguments = mapOf(
|
||||
"operation" to "multiply",
|
||||
"a" to 5.5,
|
||||
"b" to 2.0,
|
||||
"precision" to 3,
|
||||
"showSteps" to true,
|
||||
"tags" to listOf("test", "calculator", "integration"),
|
||||
)
|
||||
|
||||
val result = client.callTool(
|
||||
complexToolName,
|
||||
arguments,
|
||||
)
|
||||
|
||||
val actualContent = result.structuredContent.toString()
|
||||
val expectedContent = """
|
||||
{
|
||||
"operation" : "multiply",
|
||||
"a" : 5.5,
|
||||
"b" : 2.0,
|
||||
"result" : 11.0,
|
||||
"formattedResult" : "11.000",
|
||||
"precision" : 3,
|
||||
"tags" : ["test", "calculator", "integration"]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
actualContent shouldEqualJson expectedContent
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToolErrorHandling(): Unit = runBlocking(Dispatchers.IO) {
|
||||
val successArgs = mapOf("errorType" to "none")
|
||||
val successResult = client.callTool(errorToolName, successArgs)
|
||||
|
||||
val actualContent = successResult?.structuredContent.toString()
|
||||
val expectedContent = """
|
||||
{
|
||||
"error" : false,
|
||||
"message" : "Success"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
actualContent shouldEqualJson expectedContent
|
||||
|
||||
val errorArgs = mapOf(
|
||||
"errorType" to "error",
|
||||
"message" to "Custom error message",
|
||||
)
|
||||
val errorResult = client.callTool(
|
||||
errorToolName,
|
||||
errorArgs,
|
||||
)
|
||||
|
||||
val actualError = errorResult.structuredContent.toString()
|
||||
val expectedError = """
|
||||
{
|
||||
"error" : true,
|
||||
"message" : "Custom error message"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
actualError shouldEqualJson expectedError
|
||||
|
||||
val exceptionArgs = mapOf(
|
||||
"errorType" to "exception",
|
||||
"message" to "My exception message",
|
||||
)
|
||||
|
||||
val exceptionResult = client.callTool(
|
||||
errorToolName,
|
||||
exceptionArgs,
|
||||
)
|
||||
|
||||
assertTrue(exceptionResult.isError ?: false, "isError should be true for exception")
|
||||
|
||||
val exceptionContent = exceptionResult.content.firstOrNull { it is TextContent } as? TextContent
|
||||
assertNotNull(exceptionContent, "Error content should be present in the result")
|
||||
|
||||
val exceptionText = exceptionContent.text ?: ""
|
||||
assertTrue(
|
||||
exceptionText.contains("Error executing tool") && exceptionText.contains("My exception message"),
|
||||
"Error message should contain the exception details",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultiContentTool(): Unit = runBlocking(Dispatchers.IO) {
|
||||
val testText = "Test multi-content"
|
||||
val arguments = mapOf(
|
||||
"text" to testText,
|
||||
"includeImage" to true,
|
||||
)
|
||||
|
||||
val result = client.callTool(
|
||||
multiContentToolName,
|
||||
arguments,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
2,
|
||||
result.content.size,
|
||||
"Tool result should have 2 content items",
|
||||
)
|
||||
|
||||
val textContent = result.content.firstOrNull { it is TextContent } as? TextContent
|
||||
assertNotNull(textContent, "Result should contain TextContent")
|
||||
assertNotNull(textContent.text, "Text content should not be null")
|
||||
assertEquals(
|
||||
"Text content: $testText",
|
||||
textContent.text,
|
||||
"Text content should match",
|
||||
)
|
||||
|
||||
val imageContent = result.content.firstOrNull { it is ImageContent } as? ImageContent
|
||||
assertNotNull(imageContent, "Result should contain ImageContent")
|
||||
assertEquals("image/png", imageContent.mimeType, "Image MIME type should match")
|
||||
assertTrue(imageContent.data.isNotEmpty(), "Image data should not be empty")
|
||||
|
||||
val actualContent = result.structuredContent.toString()
|
||||
val expectedContent = """
|
||||
{
|
||||
"text" : "Test multi-content",
|
||||
"includeImage" : true
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
actualContent shouldEqualJson expectedContent
|
||||
|
||||
val textOnlyArgs = mapOf(
|
||||
"text" to testText,
|
||||
"includeImage" to false,
|
||||
)
|
||||
|
||||
val textOnlyResult = client.callTool(
|
||||
multiContentToolName,
|
||||
textOnlyArgs,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
1,
|
||||
textOnlyResult.content.size,
|
||||
"Text-only result should have 1 content item",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testComplexNestedSchema(): Unit = runBlocking(Dispatchers.IO) {
|
||||
val userJson = buildJsonObject {
|
||||
put("name", JsonPrimitive("John Galt"))
|
||||
put("age", JsonPrimitive(30))
|
||||
put(
|
||||
"address",
|
||||
buildJsonObject {
|
||||
put("street", JsonPrimitive("123 Main St"))
|
||||
put("city", JsonPrimitive("New York"))
|
||||
put("country", JsonPrimitive("USA"))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val optionsJson = buildJsonArray {
|
||||
add(JsonPrimitive("option1"))
|
||||
add(JsonPrimitive("option2"))
|
||||
add(JsonPrimitive("option3"))
|
||||
}
|
||||
|
||||
val arguments = buildJsonObject {
|
||||
put("user", userJson)
|
||||
put("options", optionsJson)
|
||||
}
|
||||
|
||||
val result = client.callTool(
|
||||
CallToolRequest(
|
||||
CallToolRequestParams(
|
||||
name = complexToolName,
|
||||
arguments = arguments,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val actualContent = result.structuredContent.toString()
|
||||
val expectedContent = """
|
||||
{
|
||||
"operation": "add",
|
||||
"a": 0.0,
|
||||
"b": 0.0,
|
||||
"result": 0.0,
|
||||
"formattedResult": "0.00",
|
||||
"precision": 2,
|
||||
"tags": []
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
actualContent shouldEqualJson expectedContent
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLargeResponse(): Unit = runBlocking(Dispatchers.IO) {
|
||||
val size = 10
|
||||
val arguments = mapOf("size" to size)
|
||||
|
||||
val result = client.callTool(
|
||||
largeToolName,
|
||||
arguments,
|
||||
)
|
||||
|
||||
val content = result.content.firstOrNull() as TextContent
|
||||
assertNotNull(content, "Tool result content should be TextContent")
|
||||
|
||||
val actualContent = result.structuredContent.toString()
|
||||
val expectedContent = """
|
||||
{
|
||||
"size" : 10000
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
actualContent shouldEqualJson expectedContent
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSlowTool(): Unit = runBlocking(Dispatchers.IO) {
|
||||
val delay = 500
|
||||
val arguments = mapOf("delay" to delay)
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
val result = client.callTool(slowToolName, arguments)
|
||||
val endTime = System.currentTimeMillis()
|
||||
|
||||
val content = result.content.firstOrNull() as? TextContent
|
||||
assertNotNull(content, "Tool result content should be TextContent")
|
||||
|
||||
assertTrue(endTime - startTime >= delay, "Tool should take at least the specified delay")
|
||||
|
||||
val actualContent = result.structuredContent.toString()
|
||||
val expectedContent = """
|
||||
{
|
||||
"delay" : 500
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
actualContent shouldEqualJson expectedContent
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSpecialCharacters() {
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val arguments = mapOf("special" to specialCharsContent)
|
||||
|
||||
val result = client.callTool(
|
||||
specialCharsToolName,
|
||||
arguments,
|
||||
)
|
||||
|
||||
val content = result.content.firstOrNull() as? TextContent
|
||||
assertNotNull(content, "Tool result content should be TextContent")
|
||||
val text = content.text ?: ""
|
||||
|
||||
assertTrue(text.contains(specialCharsContent), "Result should contain the special characters")
|
||||
|
||||
val actualContent = result.structuredContent.toString()
|
||||
val expectedContent = """
|
||||
{
|
||||
"special" : "!@#$%^&*()_+{}|:\"<>?~`-=[]\\;',./\n\t",
|
||||
"length" : 34
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
actualContent shouldEqualJson expectedContent
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConcurrentToolCalls() = runTest {
|
||||
val concurrentCount = 10
|
||||
val results = mutableListOf<io.modelcontextprotocol.kotlin.sdk.types.CallToolResult?>()
|
||||
|
||||
runBlocking {
|
||||
repeat(concurrentCount) { index ->
|
||||
launch {
|
||||
val toolName = when (index % 5) {
|
||||
0 -> basicToolName
|
||||
1 -> complexToolName
|
||||
2 -> largeToolName
|
||||
3 -> slowToolName
|
||||
else -> specialCharsToolName
|
||||
}
|
||||
|
||||
val arguments = when (toolName) {
|
||||
basicToolName -> mapOf("text" to "Concurrent call $index")
|
||||
|
||||
complexToolName -> mapOf(
|
||||
"user" to mapOf(
|
||||
"name" to "User $index",
|
||||
"age" to 20 + index,
|
||||
"address" to mapOf(
|
||||
"street" to "Street $index",
|
||||
"city" to "City $index",
|
||||
"country" to "Country $index",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
largeToolName -> mapOf("size" to 1)
|
||||
|
||||
slowToolName -> mapOf("delay" to 100)
|
||||
|
||||
else -> mapOf("special" to "!@#$%^&*()")
|
||||
}
|
||||
|
||||
val result = client.callTool(toolName, arguments)
|
||||
|
||||
synchronized(results) {
|
||||
results.add(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(concurrentCount, results.size, "All concurrent operations should complete")
|
||||
results.forEach { result ->
|
||||
assertNotNull(result, "Result should not be null")
|
||||
assertTrue(result.content.isNotEmpty(), "Result content should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNonExistentTool() = runTest {
|
||||
val nonExistentToolName = "non-existent-tool"
|
||||
val arguments = mapOf("text" to "Test")
|
||||
|
||||
val result = runBlocking {
|
||||
client.callTool(nonExistentToolName, arguments)
|
||||
}
|
||||
|
||||
assertNotNull(result, "Tool call result should not be null")
|
||||
val callResult = result
|
||||
assertTrue(callResult.isError ?: false, "isError should be true for non-existent tool")
|
||||
|
||||
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
|
||||
assertNotNull(textContent, "Error content should be present in the result")
|
||||
|
||||
val errorText = textContent.text ?: ""
|
||||
assertTrue(
|
||||
errorText.contains("non-existent-tool") && errorText.contains("not found"),
|
||||
"Error message should indicate the tool was not found",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.sse.SSE
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.engine.EmbeddedServer
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.routing.routing
|
||||
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.server.Server
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.mcp
|
||||
import io.modelcontextprotocol.kotlin.test.utils.Retry
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.io.Sink
|
||||
import kotlinx.io.Source
|
||||
import kotlinx.io.asSink
|
||||
import kotlinx.io.asSource
|
||||
import kotlinx.io.buffered
|
||||
import org.awaitility.kotlin.await
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import java.io.PipedInputStream
|
||||
import java.io.PipedOutputStream
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import io.ktor.server.cio.CIO as ServerCIO
|
||||
import io.ktor.server.sse.SSE as ServerSSE
|
||||
|
||||
@Retry(times = 3)
|
||||
abstract class OldSchemaKotlinTestBase {
|
||||
|
||||
protected val host = "localhost"
|
||||
protected var port: Int = 0
|
||||
|
||||
protected lateinit var server: Server
|
||||
protected lateinit var client: Client
|
||||
protected lateinit var serverEngine: EmbeddedServer<*, *>
|
||||
|
||||
// Transport selection
|
||||
protected enum class TransportKind { SSE, STDIO }
|
||||
protected open val transportKind: TransportKind = TransportKind.STDIO
|
||||
|
||||
// STDIO-specific fields
|
||||
private var stdioServerTransport: StdioServerTransport? = null
|
||||
private var stdioClientInput: Source? = null
|
||||
private var stdioClientOutput: Sink? = null
|
||||
|
||||
protected abstract fun configureServerCapabilities(): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
|
||||
protected abstract fun configureServer()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
setupServer()
|
||||
if (transportKind == TransportKind.SSE) {
|
||||
await
|
||||
.ignoreExceptions()
|
||||
.until {
|
||||
port = runBlocking { serverEngine.engine.resolvedConnectors().first().port }
|
||||
port != 0
|
||||
}
|
||||
}
|
||||
runBlocking {
|
||||
setupClient()
|
||||
}
|
||||
}
|
||||
|
||||
protected suspend fun setupClient() {
|
||||
when (transportKind) {
|
||||
TransportKind.SSE -> {
|
||||
val transport = SseClientTransport(
|
||||
HttpClient(CIO) {
|
||||
install(SSE)
|
||||
},
|
||||
"http://$host:$port",
|
||||
)
|
||||
client = Client(
|
||||
Implementation("test", "1.0"),
|
||||
)
|
||||
client.connect(transport)
|
||||
}
|
||||
|
||||
TransportKind.STDIO -> {
|
||||
val input = checkNotNull(stdioClientInput) { "STDIO client input not initialized" }
|
||||
val output = checkNotNull(stdioClientOutput) { "STDIO client output not initialized" }
|
||||
val transport = StdioClientTransport(
|
||||
input = input,
|
||||
output = output,
|
||||
)
|
||||
client = Client(
|
||||
Implementation("test", "1.0"),
|
||||
)
|
||||
client.connect(transport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun setupServer() {
|
||||
val capabilities = configureServerCapabilities()
|
||||
|
||||
server = Server(
|
||||
Implementation(name = "test-server", version = "1.0"),
|
||||
ServerOptions(capabilities = capabilities),
|
||||
)
|
||||
|
||||
configureServer()
|
||||
|
||||
if (transportKind == TransportKind.SSE) {
|
||||
serverEngine = embeddedServer(ServerCIO, host = host, port = port) {
|
||||
install(ServerSSE)
|
||||
routing {
|
||||
mcp { server }
|
||||
}
|
||||
}.start(wait = false)
|
||||
} else {
|
||||
// Create in-memory stdio pipes: client->server and server->client
|
||||
val clientToServerOut = PipedOutputStream()
|
||||
val clientToServerIn = PipedInputStream(clientToServerOut)
|
||||
|
||||
val serverToClientOut = PipedOutputStream()
|
||||
val serverToClientIn = PipedInputStream(serverToClientOut)
|
||||
|
||||
// Server transport reads from client and writes to client
|
||||
val serverTransport = StdioServerTransport(
|
||||
inputStream = clientToServerIn.asSource().buffered(),
|
||||
outputStream = serverToClientOut.asSink().buffered(),
|
||||
)
|
||||
stdioServerTransport = serverTransport
|
||||
|
||||
// Prepare client-side streams for later client initialization
|
||||
stdioClientInput = serverToClientIn.asSource().buffered()
|
||||
stdioClientOutput = clientToServerOut.asSink().buffered()
|
||||
|
||||
// Start server transport by connecting the server
|
||||
runBlocking {
|
||||
server.createSession(serverTransport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
// close client
|
||||
if (::client.isInitialized) {
|
||||
try {
|
||||
runBlocking {
|
||||
withTimeout(3.seconds) {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Warning: Error during client close: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// stop server
|
||||
if (transportKind == TransportKind.SSE) {
|
||||
if (::serverEngine.isInitialized) {
|
||||
try {
|
||||
serverEngine.stop(500, 1000)
|
||||
} catch (e: Exception) {
|
||||
println("Warning: Error during server stop: ${e.message}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stdioServerTransport?.let {
|
||||
try {
|
||||
runBlocking { it.close() }
|
||||
} catch (e: Exception) {
|
||||
println("Warning: Error during stdio server stop: ${e.message}")
|
||||
} finally {
|
||||
stdioServerTransport = null
|
||||
stdioClientInput = null
|
||||
stdioClientOutput = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.sse
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.OldSchemaAbstractPromptIntegrationTest
|
||||
|
||||
class OldSchemaPromptIntegrationTestSse : OldSchemaAbstractPromptIntegrationTest() {
|
||||
override val transportKind: TransportKind = TransportKind.SSE
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.sse
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.OldSchemaAbstractResourceIntegrationTest
|
||||
|
||||
class OldSchemaResourceIntegrationTestSse : OldSchemaAbstractResourceIntegrationTest() {
|
||||
override val transportKind: TransportKind = TransportKind.SSE
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.sse
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.OldSchemaAbstractToolIntegrationTest
|
||||
|
||||
class OldSchemaToolIntegrationTestSse : OldSchemaAbstractToolIntegrationTest() {
|
||||
override val transportKind: TransportKind = TransportKind.SSE
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.stdio
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.OldSchemaAbstractPromptIntegrationTest
|
||||
|
||||
class OldSchemaPromptIntegrationTestStdio : OldSchemaAbstractPromptIntegrationTest() {
|
||||
override val transportKind: TransportKind = TransportKind.STDIO
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.stdio
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.OldSchemaAbstractResourceIntegrationTest
|
||||
|
||||
class OldSchemaResourceIntegrationTestStdio : OldSchemaAbstractResourceIntegrationTest() {
|
||||
override val transportKind: TransportKind = TransportKind.STDIO
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.stdio
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.OldSchemaAbstractToolIntegrationTest
|
||||
|
||||
class OldSchemaToolIntegrationTestStdio : OldSchemaAbstractToolIntegrationTest() {
|
||||
override val transportKind: TransportKind = TransportKind.STDIO
|
||||
}
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.websocket
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.cio.CIOApplicationEngine
|
||||
import io.ktor.server.engine.EmbeddedServer
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.routing.routing
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptArgument
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.Role
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.client.Client
|
||||
import io.modelcontextprotocol.kotlin.sdk.client.mcpWebSocketTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.Server
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.mcpWebSocket
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
import io.ktor.client.engine.cio.CIO as ClientCIO
|
||||
import io.ktor.client.plugins.websocket.WebSockets as ClientWebSocket
|
||||
import io.ktor.server.cio.CIO as ServerCIO
|
||||
import io.ktor.server.websocket.WebSockets as ServerWebSockets
|
||||
|
||||
class OldSchemaWebSocketIntegrationTest {
|
||||
|
||||
@Test
|
||||
fun `client should be able to connect to websocket server 2`() = runBlocking(Dispatchers.IO) {
|
||||
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
|
||||
var client: Client? = null
|
||||
|
||||
try {
|
||||
server = initServer()
|
||||
val port = server.engine.resolvedConnectors().first().port
|
||||
client = initClient(serverPort = port)
|
||||
} finally {
|
||||
client?.close()
|
||||
server?.stop(1000, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Case #1: One opened connection, a client gets a prompt
|
||||
*
|
||||
* 1. Open WebSocket from Client A.
|
||||
* 2. Send a POST request from Client A to POST /prompts/get.
|
||||
* 3. Observe that Client A receives a response related to it.
|
||||
*/
|
||||
@Test
|
||||
fun `single websocket connection`() = runBlocking(Dispatchers.IO) {
|
||||
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
|
||||
var client: Client? = null
|
||||
|
||||
try {
|
||||
server = initServer()
|
||||
val port = server.engine.resolvedConnectors().first().port
|
||||
client = initClient("Client A", port)
|
||||
|
||||
val promptA = getPrompt(client, "Client A")
|
||||
assertTrue { "Client A" in promptA }
|
||||
} finally {
|
||||
client?.close()
|
||||
server?.stop(1000, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Case #1: Two open connections, each client gets a client-specific prompt
|
||||
*
|
||||
* 1. Open WebSocket connection #1 from Client A and note the sessionId=<sessionId#1> value.
|
||||
* 2. Open WebSocket connection #2 from Client B and note the sessionId=<sessionId#2> value.
|
||||
* 3. Send a POST request to POST /message with the corresponding sessionId#1.
|
||||
* 4. Observe that Client B (connection #2) receives a response related to sessionId#1.
|
||||
*/
|
||||
@Test
|
||||
fun `multiple websocket connections`() = runBlocking(Dispatchers.IO) {
|
||||
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
|
||||
var clientA: Client? = null
|
||||
var clientB: Client? = null
|
||||
|
||||
try {
|
||||
server = initServer()
|
||||
val port = server.engine.resolvedConnectors().first().port
|
||||
clientA = initClient("Client A", port)
|
||||
clientB = initClient("Client B", port)
|
||||
|
||||
// Step 3: Send a prompt request from Client A
|
||||
val promptA = getPrompt(clientA, "Client A")
|
||||
// Step 4: Send a prompt request from Client B
|
||||
val promptB = getPrompt(clientB, "Client B")
|
||||
|
||||
assertTrue { "Client A" in promptA }
|
||||
assertTrue { "Client B" in promptB }
|
||||
} finally {
|
||||
clientA?.close()
|
||||
clientB?.close()
|
||||
server?.stop(1000, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initClient(name: String = "", serverPort: Int): Client {
|
||||
val client = Client(
|
||||
Implementation(name = name, version = "1.0.0"),
|
||||
)
|
||||
|
||||
val httpClient = HttpClient(ClientCIO) {
|
||||
install(ClientWebSocket)
|
||||
}
|
||||
|
||||
// Create a transport wrapper that captures the session ID and received messages
|
||||
val transport = httpClient.mcpWebSocketTransport {
|
||||
url {
|
||||
host = URL
|
||||
port = serverPort
|
||||
}
|
||||
}
|
||||
|
||||
client.connect(transport)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
private fun initServer(): EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration> {
|
||||
val server = Server(
|
||||
Implementation(name = "websocket-server", version = "1.0.0"),
|
||||
ServerOptions(
|
||||
capabilities = ServerCapabilities(prompts = ServerCapabilities.Prompts(listChanged = true)),
|
||||
),
|
||||
)
|
||||
|
||||
server.addPrompt(
|
||||
name = "prompt",
|
||||
description = "Prompt description",
|
||||
arguments = listOf(
|
||||
PromptArgument(
|
||||
name = "client",
|
||||
description = "Client name who requested a prompt",
|
||||
required = true,
|
||||
),
|
||||
),
|
||||
) { request ->
|
||||
GetPromptResult(
|
||||
description = "Prompt for ${request.params.name}",
|
||||
messages = listOf(
|
||||
PromptMessage(
|
||||
role = Role.user,
|
||||
content = TextContent("Prompt for client ${request.params.arguments?.get("client")}"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val ktorServer = embeddedServer(ServerCIO, host = URL, port = PORT) {
|
||||
install(ServerWebSockets)
|
||||
routing {
|
||||
mcpWebSocket(block = { server })
|
||||
}
|
||||
}
|
||||
|
||||
return ktorServer.start(wait = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a prompt result using the provided client and client name.
|
||||
*/
|
||||
private suspend fun getPrompt(client: Client, clientName: String): String {
|
||||
val response = client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
"prompt",
|
||||
arguments = mapOf("client" to clientName),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return (response.messages.first().content as? TextContent)?.text
|
||||
?: error("Failed to receive prompt for Client $clientName")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PORT = 0
|
||||
private const val URL = "127.0.0.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package io.modelcontextprotocol.kotlin.sdk.integration.sse
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.sse.SSE
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.cio.CIOApplicationEngine
|
||||
import io.ktor.server.engine.EmbeddedServer
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.routing.routing
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptArgument
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.Role
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.client.Client
|
||||
import io.modelcontextprotocol.kotlin.sdk.client.mcpSseTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.Server
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.mcp
|
||||
import io.ktor.client.engine.cio.CIO as ClientCIO
|
||||
import io.ktor.server.cio.CIO as ServerCIO
|
||||
import io.ktor.server.sse.SSE as ServerSSE
|
||||
|
||||
open class OldSchemaAbstractSseIntegrationTest {
|
||||
|
||||
suspend fun initTestClient(serverPort: Int, name: String? = null): Client {
|
||||
val client = Client(
|
||||
Implementation(name = name ?: DEFAULT_CLIENT_NAME, version = VERSION),
|
||||
)
|
||||
|
||||
val httpClient = HttpClient(ClientCIO) {
|
||||
install(SSE)
|
||||
}
|
||||
|
||||
// Create a transport wrapper that captures the session ID and received messages
|
||||
val transport = httpClient.mcpSseTransport {
|
||||
url {
|
||||
host = URL
|
||||
port = serverPort
|
||||
}
|
||||
}
|
||||
|
||||
client.connect(transport)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
suspend fun initTestServer(
|
||||
name: String? = null,
|
||||
): EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration> {
|
||||
val server = Server(
|
||||
Implementation(name = name ?: DEFAULT_SERVER_NAME, version = VERSION),
|
||||
ServerOptions(
|
||||
capabilities = ServerCapabilities(prompts = ServerCapabilities.Prompts(listChanged = true)),
|
||||
),
|
||||
) {
|
||||
addPrompt(
|
||||
name = "prompt",
|
||||
description = "Prompt description",
|
||||
arguments = listOf(
|
||||
PromptArgument(
|
||||
name = "client",
|
||||
description = "Client name who requested a prompt",
|
||||
required = true,
|
||||
),
|
||||
),
|
||||
) { request ->
|
||||
GetPromptResult(
|
||||
description = "Prompt for ${request.params.name}",
|
||||
messages = listOf(
|
||||
PromptMessage(
|
||||
role = Role.user,
|
||||
content = TextContent("Prompt for client ${request.params.arguments?.get("client")}"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val ktorServer = embeddedServer(
|
||||
ServerCIO,
|
||||
host = URL,
|
||||
port = PORT,
|
||||
) {
|
||||
install(ServerSSE)
|
||||
routing {
|
||||
mcp { server }
|
||||
}
|
||||
}
|
||||
|
||||
return ktorServer.startSuspend(wait = false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_CLIENT_NAME = "sse-test-client"
|
||||
private const val DEFAULT_SERVER_NAME = "sse-test-server"
|
||||
private const val VERSION = "1.0.0"
|
||||
private const val URL = "127.0.0.1"
|
||||
private const val PORT = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package io.modelcontextprotocol.kotlin.sdk.integration.sse
|
||||
|
||||
import io.ktor.server.cio.CIOApplicationEngine
|
||||
import io.ktor.server.engine.EmbeddedServer
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.client.Client
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams
|
||||
import io.modelcontextprotocol.kotlin.test.utils.actualPort
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class OldSchemaSseIntegrationTest : OldSchemaAbstractSseIntegrationTest() {
|
||||
|
||||
@Test
|
||||
fun `client should be able to connect to sse server`() = runBlocking(Dispatchers.IO) {
|
||||
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
|
||||
var client: Client? = null
|
||||
|
||||
try {
|
||||
server = initTestServer()
|
||||
val port = server.actualPort()
|
||||
client = initTestClient(serverPort = port)
|
||||
} finally {
|
||||
client?.close()
|
||||
server?.stopSuspend(1000, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Case #1: One opened connection, a client gets a prompt
|
||||
*
|
||||
* 1. Open SSE from Client A.
|
||||
* 2. Send a POST request from Client A to POST /prompts/get.
|
||||
* 3. Observe that Client A receives a response related to it.
|
||||
*/
|
||||
@Test
|
||||
fun `single sse connection`() = runBlocking(Dispatchers.IO) {
|
||||
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
|
||||
var client: Client? = null
|
||||
try {
|
||||
server = initTestServer()
|
||||
val port = server.actualPort()
|
||||
client = initTestClient(port, "Client A")
|
||||
|
||||
val promptA = getPrompt(client, "Client A")
|
||||
assertTrue { "Client A" in promptA }
|
||||
} finally {
|
||||
client?.close()
|
||||
server?.stopSuspend(1000, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Case #1: Two open connections, each client gets a client-specific prompt
|
||||
*
|
||||
* 1. Open SSE connection #1 from Client A and note the sessionId=<sessionId#1> value.
|
||||
* 2. Open SSE connection #2 from Client B and note the sessionId=<sessionId#2> value.
|
||||
* 3. Send a POST request to POST /message with the corresponding sessionId#1.
|
||||
* 4. Observe that Client B (connection #2) receives a response related to sessionId#1.
|
||||
*/
|
||||
@Test
|
||||
fun `multiple sse connections`() = runBlocking(Dispatchers.IO) {
|
||||
var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
|
||||
var clientA: Client? = null
|
||||
var clientB: Client? = null
|
||||
|
||||
try {
|
||||
server = initTestServer()
|
||||
val port = server.engine.resolvedConnectors().first().port
|
||||
|
||||
clientA = initTestClient(port, "Client A")
|
||||
clientB = initTestClient(port, "Client B")
|
||||
|
||||
// Step 3: Send a prompt request from Client A
|
||||
val promptA = getPrompt(clientA, "Client A")
|
||||
// Step 4: Send a prompt request from Client B
|
||||
val promptB = getPrompt(clientB, "Client B")
|
||||
|
||||
assertTrue { "Client A" in promptA }
|
||||
assertTrue { "Client B" in promptB }
|
||||
} finally {
|
||||
clientA?.close()
|
||||
clientB?.close()
|
||||
server?.stopSuspend(1000, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a prompt result using the provided client and client name.
|
||||
*/
|
||||
private suspend fun getPrompt(client: Client, clientName: String): String {
|
||||
val response = client.getPrompt(
|
||||
GetPromptRequest(
|
||||
GetPromptRequestParams(
|
||||
"prompt",
|
||||
arguments = mapOf("client" to clientName),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return (response.messages.first().content as? TextContent)?.text
|
||||
?: error("Failed to receive prompt for Client $clientName")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration.typescript
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.client.Client
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Timeout
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
abstract class OldSchemaAbstractKotlinClientTsServerTest : TsTestBase() {
|
||||
protected abstract suspend fun <T> useClient(block: suspend (Client) -> T): T
|
||||
|
||||
@Test
|
||||
@Timeout(30, unit = TimeUnit.SECONDS)
|
||||
fun connectsAndPings() = runBlocking(Dispatchers.IO) {
|
||||
useClient { client ->
|
||||
assertNotNull(client, "Client should be initialized")
|
||||
val ping = client.ping()
|
||||
assertNotNull(ping, "Ping result should not be null")
|
||||
val serverImpl = client.serverVersion
|
||||
assertNotNull(serverImpl, "Server implementation should not be null")
|
||||
println("Connected to TypeScript server: ${serverImpl.name} v${serverImpl.version}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(30, unit = TimeUnit.SECONDS)
|
||||
fun listsTools() = runBlocking(Dispatchers.IO) {
|
||||
useClient { client ->
|
||||
val result = client.listTools()
|
||||
assertNotNull(result, "Tools list should not be null")
|
||||
assertTrue(result.tools.isNotEmpty(), "Tools list should not be empty")
|
||||
val toolNames = result.tools.map { it.name }
|
||||
assertTrue("greet" in toolNames, "Greet tool should be available")
|
||||
assertTrue("multi-greet" in toolNames, "Multi-greet tool should be available")
|
||||
// Some tests also check collect-user-info; keep base minimal and non-breaking
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(30, unit = TimeUnit.SECONDS)
|
||||
fun callGreet() = runBlocking(Dispatchers.IO) {
|
||||
useClient { client ->
|
||||
val testName = "TestUser"
|
||||
val arguments = mapOf("name" to testName)
|
||||
val result = client.callTool("greet", arguments)
|
||||
assertNotNull(result, "Tool call result should not be null")
|
||||
val callResult = result as io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
|
||||
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
|
||||
assertNotNull(textContent, "Text content should be present in the result")
|
||||
assertEquals("Hello, $testName!", textContent.text)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(30, unit = TimeUnit.SECONDS)
|
||||
fun multipleClients() = runBlocking(Dispatchers.IO) {
|
||||
useClient { client1 ->
|
||||
useClient { client2 ->
|
||||
val tools1 = client1.listTools()
|
||||
val tools2 = client2.listTools()
|
||||
assertTrue(tools1.tools.isNotEmpty(), "Tools list for first client should not be empty")
|
||||
assertTrue(tools2.tools.isNotEmpty(), "Tools list for second client should not be empty")
|
||||
val toolNames1 = tools1.tools.map { it.name }
|
||||
val toolNames2 = tools2.tools.map { it.name }
|
||||
assertTrue("greet" in toolNames1, "Greet tool should be available to first client")
|
||||
assertTrue("multi-greet" in toolNames1, "Multi-greet tool should be available to first client")
|
||||
assertTrue("greet" in toolNames2, "Greet tool should be available to second client")
|
||||
assertTrue("multi-greet" in toolNames2, "Multi-greet tool should be available to second client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.client.Client
|
||||
import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind
|
||||
import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Timeout
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class OldSchemaKotlinClientTsServerEdgeCasesTestSse : TsTestBase() {
|
||||
|
||||
override val transportKind = TransportKind.SSE
|
||||
|
||||
private val host = "localhost"
|
||||
private val serverUrl: String by lazy { getSharedSseUrl() }
|
||||
|
||||
private lateinit var client: Client
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
if (::client.isInitialized) {
|
||||
try {
|
||||
runBlocking {
|
||||
withTimeout(3.seconds) {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Warning: Error during client close: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(30, unit = TimeUnit.SECONDS)
|
||||
fun testNonExistentTool(): Unit = runBlocking(Dispatchers.IO) {
|
||||
withClient(serverUrl) { client ->
|
||||
val nonExistentToolName = "non-existent-tool"
|
||||
val arguments = mapOf("name" to "TestUser")
|
||||
|
||||
val result = client.callTool(nonExistentToolName, arguments)
|
||||
assertNotNull(result, "Tool call result should not be null")
|
||||
|
||||
val callResult = result as io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
|
||||
assertTrue(callResult.isError ?: false, "isError should be true for non-existent tool")
|
||||
|
||||
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
|
||||
assertNotNull(textContent, "Error content should be present in the result")
|
||||
|
||||
val errorText = textContent.text ?: ""
|
||||
assertTrue(
|
||||
errorText.contains("non-existent-tool") && errorText.contains("not found"),
|
||||
"Error message should indicate the tool was not found",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(30, unit = TimeUnit.SECONDS)
|
||||
fun testSpecialCharactersInArguments(): Unit = runBlocking(Dispatchers.IO) {
|
||||
withClient(serverUrl) { client ->
|
||||
val specialChars = "!@#$%^&*()_+{}[]|\\:;\"'<>,.?/"
|
||||
val arguments = mapOf("name" to specialChars)
|
||||
|
||||
val result = client.callTool("greet", arguments)
|
||||
assertNotNull(result, "Tool call result should not be null")
|
||||
|
||||
val callResult = result as io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
|
||||
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
|
||||
assertNotNull(textContent, "Text content should be present in the result")
|
||||
|
||||
val text = textContent.text ?: ""
|
||||
assertTrue(
|
||||
text.contains(specialChars),
|
||||
"Tool response should contain the special characters",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(30, unit = TimeUnit.SECONDS)
|
||||
fun testLargePayload(): Unit = runBlocking(Dispatchers.IO) {
|
||||
withClient(serverUrl) { client ->
|
||||
val largeName = "A".repeat(10 * 1024)
|
||||
val arguments = mapOf("name" to largeName)
|
||||
|
||||
val result = client.callTool("greet", arguments)
|
||||
assertNotNull(result, "Tool call result should not be null")
|
||||
|
||||
val callResult = result as io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
|
||||
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
|
||||
assertNotNull(textContent, "Text content should be present in the result")
|
||||
|
||||
val text = textContent.text ?: ""
|
||||
assertTrue(
|
||||
text.contains("Hello,") && text.contains("A"),
|
||||
"Tool response should contain the greeting with the large name",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(60, unit = TimeUnit.SECONDS)
|
||||
fun testConcurrentRequests(): Unit = runBlocking(Dispatchers.IO) {
|
||||
withClient(serverUrl) { client ->
|
||||
val concurrentCount = 5
|
||||
val responses = coroutineScope {
|
||||
val results = (1..concurrentCount).map { i ->
|
||||
async {
|
||||
val name = "ConcurrentClient$i"
|
||||
val arguments = mapOf("name" to name)
|
||||
|
||||
val result = client.callTool("greet", arguments)
|
||||
assertNotNull(result, "Tool call result should not be null for client $i")
|
||||
|
||||
val callResult = result as io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
|
||||
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
|
||||
assertNotNull(textContent, "Text content should be present for client $i")
|
||||
|
||||
textContent.text ?: ""
|
||||
}
|
||||
}
|
||||
results.awaitAll()
|
||||
}
|
||||
|
||||
for (i in 1..concurrentCount) {
|
||||
val expectedName = "ConcurrentClient$i"
|
||||
val matchingResponses = responses.filter { it.contains("Hello, $expectedName!") }
|
||||
assertEquals(
|
||||
1,
|
||||
matchingResponses.size,
|
||||
"Should have exactly one response for $expectedName",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(30, unit = TimeUnit.SECONDS)
|
||||
fun testInvalidArguments(): Unit = runBlocking(Dispatchers.IO) {
|
||||
withClient(serverUrl) { client ->
|
||||
val invalidArguments = mapOf(
|
||||
"name" to JsonObject(mapOf("nested" to JsonPrimitive("value"))),
|
||||
)
|
||||
|
||||
val result = client.callTool("greet", invalidArguments)
|
||||
assertNotNull(result, "Tool call result should not be null")
|
||||
|
||||
val callResult = result as io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
|
||||
assertTrue(callResult.isError ?: false, "isError should be true for invalid arguments")
|
||||
|
||||
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
|
||||
assertNotNull(textContent, "Error content should be present in the result")
|
||||
|
||||
val errorText = textContent.text ?: ""
|
||||
assertTrue(
|
||||
errorText.contains("Invalid arguments") && errorText.contains("greet"),
|
||||
"Error message should indicate invalid arguments for tool greet",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(30, unit = TimeUnit.SECONDS)
|
||||
fun testMultipleToolCalls(): Unit = runBlocking(Dispatchers.IO) {
|
||||
withClient(serverUrl) { client ->
|
||||
repeat(10) { i ->
|
||||
val name = "SequentialClient$i"
|
||||
val arguments = mapOf("name" to name)
|
||||
|
||||
val result = client.callTool("greet", arguments)
|
||||
assertNotNull(result, "Tool call result should not be null for call $i")
|
||||
|
||||
val callResult = result as io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
|
||||
val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent
|
||||
assertNotNull(textContent, "Text content should be present for call $i")
|
||||
|
||||
assertEquals(
|
||||
"Hello, $name!",
|
||||
textContent.text,
|
||||
"Tool response should contain the greeting with the provided name",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue