Skip to content

Commit dfa245f

Browse files
committed
test(integration): add pagination and invalid cursor tests for Streamable HTTP
Add integration tests for cursor-based pagination and invalid cursor handling across Prompts, Resources, and Tools endpoints. - Pagination tests iterate all pages until nextCursor is null - Invalid cursor tests use assertFailsWith to verify McpException - Remove duplicate LoggingIntegrationTestStreamableHttp (covered by ClientConnectionLoggingTest)
1 parent fd3a858 commit dfa245f

3 files changed

Lines changed: 187 additions & 0 deletions

File tree

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import io.kotest.matchers.string.shouldContain
66
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest
77
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams
88
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult
9+
import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsRequest
10+
import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsResult
911
import io.modelcontextprotocol.kotlin.sdk.types.McpException
12+
import io.modelcontextprotocol.kotlin.sdk.types.Method
13+
import io.modelcontextprotocol.kotlin.sdk.types.RPCError
14+
import io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequestParams
15+
import io.modelcontextprotocol.kotlin.sdk.types.Prompt
1016
import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument
1117
import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage
1218
import io.modelcontextprotocol.kotlin.sdk.types.Role
@@ -19,6 +25,7 @@ import kotlinx.coroutines.test.runTest
1925
import org.junit.jupiter.api.Test
2026
import org.junit.jupiter.api.assertThrows
2127
import kotlin.test.assertEquals
28+
import kotlin.test.assertFailsWith
2229
import kotlin.test.assertNotNull
2330
import kotlin.test.assertTrue
2431

@@ -697,4 +704,56 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() {
697704
exception.message shouldBe expectedMessage
698705
}
699706
}
707+
708+
@Test
709+
fun testListPromptsPagination() = runBlocking(Dispatchers.IO) {
710+
val pagePrefix = "paginated-prompt-"
711+
(0 until 5).forEach { i ->
712+
val name = "$pagePrefix$i"
713+
server.addPrompt(name = name, description = "desc", arguments = listOf()) { _ ->
714+
GetPromptResult(description = "desc", messages = listOf(PromptMessage(role = Role.Assistant, content = TextContent(text = name))))
715+
}
716+
}
717+
718+
server.sessions.forEach { (_, session) ->
719+
session.setRequestHandler<ListPromptsRequest>(Method.Defined.PromptsList) { request, _ ->
720+
val all = server.prompts.values.map { it.prompt }
721+
val cursor = request.cursor?.toIntOrNull() ?: 0
722+
val pageSize = 2
723+
val page = all.drop(cursor).take(pageSize)
724+
val next = if (cursor + page.size < all.size) (cursor + page.size).toString() else null
725+
ListPromptsResult(prompts = page, nextCursor = next)
726+
}
727+
}
728+
729+
val allPrompts = mutableListOf<Prompt>()
730+
var currentCursor: String? = null
731+
do {
732+
val request = if (currentCursor == null) ListPromptsRequest() else ListPromptsRequest(PaginatedRequestParams(cursor = currentCursor))
733+
val response = client.listPrompts(request)
734+
allPrompts.addAll(response.prompts)
735+
currentCursor = response.nextCursor
736+
} while (currentCursor != null)
737+
738+
val paginatedPrompts = allPrompts.filter { it.name.startsWith(pagePrefix) }
739+
assertEquals(5, paginatedPrompts.size, "Should have collected all 5 paginated prompts")
740+
}
741+
742+
@Test
743+
fun testListPromptsInvalidCursor() = runBlocking(Dispatchers.IO) {
744+
server.sessions.forEach { (_, session) ->
745+
session.setRequestHandler<ListPromptsRequest>(Method.Defined.PromptsList) { request, _ ->
746+
val cursor = request.cursor?.toIntOrNull() ?: throw IllegalArgumentException("Invalid cursor")
747+
val all = server.prompts.values.map { it.prompt }
748+
val page = all.drop(cursor).take(2)
749+
ListPromptsResult(prompts = page, nextCursor = null)
750+
}
751+
}
752+
753+
val exception = assertFailsWith<McpException> {
754+
client.listPrompts(ListPromptsRequest(PaginatedRequestParams(cursor = "not-a-number")))
755+
}
756+
757+
assertEquals(RPCError.ErrorCode.INTERNAL_ERROR, exception.code)
758+
}
700759
}

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin
22

33
import io.modelcontextprotocol.kotlin.sdk.types.BlobResourceContents
4+
import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest
5+
import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult
46
import io.modelcontextprotocol.kotlin.sdk.types.McpException
7+
import io.modelcontextprotocol.kotlin.sdk.types.Method
8+
import io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequestParams
59
import io.modelcontextprotocol.kotlin.sdk.types.RPCError
610
import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest
711
import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams
@@ -20,6 +24,7 @@ import org.junit.jupiter.api.Test
2024
import org.junit.jupiter.api.assertThrows
2125
import java.util.concurrent.atomic.AtomicBoolean
2226
import kotlin.test.assertEquals
27+
import kotlin.test.assertFailsWith
2328
import kotlin.test.assertNotNull
2429
import kotlin.test.assertTrue
2530

@@ -309,4 +314,62 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() {
309314
assertTrue(result.contents.isNotEmpty(), "Result contents should not be empty")
310315
}
311316
}
317+
318+
@Test
319+
fun testListResourcesPagination() = runBlocking(Dispatchers.IO) {
320+
val prefix = "paginated-resource-"
321+
(0 until 6).forEach { i ->
322+
val uri = "test://$prefix$i.txt"
323+
server.addResource(uri = uri, name = "Name-$i", description = "desc", mimeType = "text/plain") { request ->
324+
ReadResourceResult(contents = listOf(TextResourceContents(text = uri, uri = request.params.uri, mimeType = "text/plain")))
325+
}
326+
}
327+
328+
server.sessions.forEach { (_, session) ->
329+
session.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { request, _ ->
330+
val all = server.resources.values.map { it.resource }
331+
val cursor = request.cursor?.toIntOrNull() ?: 0
332+
val pageSize = 3
333+
val page = all.drop(cursor).take(pageSize)
334+
val next = if (cursor + page.size < all.size) (cursor + page.size).toString() else null
335+
ListResourcesResult(resources = page, nextCursor = next)
336+
}
337+
}
338+
339+
val combinedUris = mutableListOf<String>()
340+
var currentCursor: String? = null
341+
342+
do {
343+
val request = if (currentCursor == null) {
344+
ListResourcesRequest()
345+
} else {
346+
ListResourcesRequest(PaginatedRequestParams(cursor = currentCursor))
347+
}
348+
349+
val response = client.listResources(request)
350+
combinedUris += response.resources.map { it.uri }
351+
currentCursor = response.nextCursor
352+
} while (currentCursor != null)
353+
354+
val paginatedResources = combinedUris.filter { it.contains(prefix) }
355+
assertEquals(6, paginatedResources.size, "Should have collected all 6 paginated resources")
356+
}
357+
358+
@Test
359+
fun testListResourcesInvalidCursor() = runBlocking(Dispatchers.IO) {
360+
server.sessions.forEach { (_, session) ->
361+
session.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { request, _ ->
362+
val cursor = request.cursor?.toIntOrNull() ?: throw IllegalArgumentException("Invalid cursor")
363+
val all = server.resources.values.map { it.resource }
364+
val page = all.drop(cursor).take(2)
365+
ListResourcesResult(resources = page, nextCursor = null)
366+
}
367+
}
368+
369+
val exception = assertFailsWith<McpException> {
370+
client.listResources(ListResourcesRequest(PaginatedRequestParams(cursor = "bad")))
371+
}
372+
373+
assertEquals(RPCError.ErrorCode.INTERNAL_ERROR, exception.code)
374+
}
312375
}

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams
66
import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
77
import io.modelcontextprotocol.kotlin.sdk.types.ContentBlock
88
import io.modelcontextprotocol.kotlin.sdk.types.ImageContent
9+
import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest
10+
import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult
11+
import io.modelcontextprotocol.kotlin.sdk.types.McpException
12+
import io.modelcontextprotocol.kotlin.sdk.types.Method
13+
import io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequestParams
14+
import io.modelcontextprotocol.kotlin.sdk.types.RPCError
915
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
1016
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
1117
import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
@@ -25,6 +31,7 @@ import java.text.DecimalFormat
2531
import java.text.DecimalFormatSymbols
2632
import java.util.Locale
2733
import kotlin.test.assertEquals
34+
import kotlin.test.assertFailsWith
2835
import kotlin.test.assertNotNull
2936
import kotlin.test.assertTrue
3037

@@ -791,4 +798,62 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() {
791798
"Error message should indicate the tool was not found",
792799
)
793800
}
801+
802+
@Test
803+
fun testListToolsPagination() = runBlocking(Dispatchers.IO) {
804+
val prefix = "paginated-tool-"
805+
(0 until 5).forEach { i ->
806+
val name = "$prefix$i"
807+
server.addTool(name = name, description = "desc") { request ->
808+
CallToolResult(content = listOf(TextContent(text = name)), structuredContent = buildJsonObject { put("name", name) })
809+
}
810+
}
811+
812+
server.sessions.forEach { (_, session) ->
813+
session.setRequestHandler<ListToolsRequest>(Method.Defined.ToolsList) { request, _ ->
814+
val all = server.tools.values.map { it.tool }
815+
val cursor = request.cursor?.toIntOrNull() ?: 0
816+
val pageSize = 2
817+
val page = all.drop(cursor).take(pageSize)
818+
val next = if (cursor + page.size < all.size) (cursor + page.size).toString() else null
819+
ListToolsResult(tools = page, nextCursor = next)
820+
}
821+
}
822+
823+
val combinedNames = mutableListOf<String>()
824+
var currentCursor: String? = null
825+
826+
do {
827+
val request = if (currentCursor == null) {
828+
ListToolsRequest()
829+
} else {
830+
ListToolsRequest(PaginatedRequestParams(cursor = currentCursor))
831+
}
832+
833+
val response = client.listTools(request)
834+
combinedNames += response.tools.map { it.name }
835+
currentCursor = response.nextCursor
836+
} while (currentCursor != null)
837+
838+
val paginatedTools = combinedNames.filter { it.startsWith(prefix) }
839+
assertEquals(5, paginatedTools.size, "Should have collected all 5 paginated tools")
840+
}
841+
842+
@Test
843+
fun testListToolsInvalidCursor() = runBlocking(Dispatchers.IO) {
844+
server.sessions.forEach { (_, session) ->
845+
session.setRequestHandler<ListToolsRequest>(Method.Defined.ToolsList) { request, _ ->
846+
val cursor = request.cursor?.toIntOrNull() ?: throw IllegalArgumentException("Invalid cursor")
847+
val all = server.tools.values.map { it.tool }
848+
val page = all.drop(cursor).take(2)
849+
ListToolsResult(tools = page)
850+
}
851+
}
852+
853+
val exception = assertFailsWith<McpException> {
854+
client.listTools(ListToolsRequest(PaginatedRequestParams(cursor = "bad")))
855+
}
856+
857+
assertEquals(RPCError.ErrorCode.INTERNAL_ERROR, exception.code)
858+
}
794859
}

0 commit comments

Comments
 (0)