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:
Pavel Gorgulov 2026-02-04 20:04:11 +01:00 • committed by GitHub
commit 92823b987f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 224 additions and 8255 deletions

View file

@ -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()
}
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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")
}
}

View file

@ -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")
}
}
}

View file

@ -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",
)
}
}

View file

@ -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
}
}
}
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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"
}
}

View file

@ -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
}
}

View file

@ -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")
}
}

View file

@ -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")
}
}
}
}

View file

@ -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