test(streamable-http): add missing integration tests for pagination, bad request, and logging#613
Conversation
There was a problem hiding this comment.
Pull request overview
Adds missing Streamable HTTP integration coverage to close remaining non-security scenarios for listing pagination, invalid cursor handling, and logging message delivery/filtering.
Changes:
- Add cursor-based pagination integration tests for prompts/resources/tools list endpoints.
- Add invalid cursor (“bad request”) tests asserting server exceptions propagate as
McpExceptionwithINTERNAL_ERROR. - Add Streamable HTTP–specific logging integration tests for
setLoggingLeveland log message notifications.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/streamablehttp/LoggingIntegrationTestStreamableHttp.kt | New Streamable HTTP logging integration tests (notifications + level filtering). |
| integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt | Adds tools list pagination + invalid cursor integration coverage. |
| integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt | Adds resources list pagination + invalid cursor integration coverage. |
| integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt | Adds prompts list pagination + invalid cursor integration coverage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| val all = server.tools.values.map { it.tool } | ||
| val cursor = request.cursor?.toIntOrNull() ?: 0 | ||
| val pageSize = 2 | ||
| val page = all.drop(cursor).take(pageSize) | ||
| val next = if (cursor + page.size < all.size) (cursor + page.size).toString() else null | ||
| ListToolsResult(tools = page, nextCursor = next) | ||
| } | ||
| } | ||
|
|
||
| val first = client.listTools() | ||
| assertTrue(first.tools.isNotEmpty()) | ||
| val next = first.nextCursor | ||
| assertNotNull(next) | ||
|
|
||
| val second = client.listTools(ListToolsRequest(PaginatedRequestParams(cursor = next))) | ||
| assertTrue(second.tools.isNotEmpty()) | ||
|
|
||
| val combinedNames = (first.tools + second.tools).map { it.name } | ||
| assertTrue(combinedNames.any { it.startsWith(prefix) }) | ||
| } |
There was a problem hiding this comment.
The pagination handler builds all from server.tools.values, which comes from an immutable persistent map (no guaranteed iteration order). Because the test only fetches two pages, the newly added paginated-tool-* entries may not appear in those pages, making this test potentially flaky. Consider sorting all by tool name and/or filtering to the prefix tools (or paging until nextCursor == null) so the assertions are deterministic.
| val exception = org.junit.jupiter.api.assertThrows<io.modelcontextprotocol.kotlin.sdk.types.McpException> { | ||
| runBlocking { | ||
| client.listTools(ListToolsRequest(PaginatedRequestParams(cursor = "bad"))) | ||
| } |
There was a problem hiding this comment.
This test is already running inside runBlocking(Dispatchers.IO); wrapping the suspending client.listTools(...) call in an additional runBlocking { ... } inside assertThrows is redundant and can make failures/hangs harder to diagnose. Prefer using a suspending assertion (e.g., assertFailsWith) inside the existing runBlocking, or drop the outer runBlocking and keep a single runBlocking inside assertThrows.
| val first = client.listResources() | ||
| assertTrue(first.resources.isNotEmpty()) | ||
| val next = first.nextCursor | ||
| assertNotNull(next) | ||
|
|
||
| val second = client.listResources(ListResourcesRequest(PaginatedRequestParams(cursor = next))) | ||
| assertTrue(second.resources.isNotEmpty()) | ||
|
|
||
| val combinedUris = (first.resources + second.resources).map { it.uri } |
There was a problem hiding this comment.
The pagination handler uses server.resources.values (backed by a persistent map) as the source list. Iteration order isn’t guaranteed, and since the test only fetches two pages, the paginated-resource-* entries might not land in those pages, which can make the test flaky. Sort/filter the resources list (e.g., by uri/name) and/or page until nextCursor == null before asserting.
| val first = client.listResources() | |
| assertTrue(first.resources.isNotEmpty()) | |
| val next = first.nextCursor | |
| assertNotNull(next) | |
| val second = client.listResources(ListResourcesRequest(PaginatedRequestParams(cursor = next))) | |
| assertTrue(second.resources.isNotEmpty()) | |
| val combinedUris = (first.resources + second.resources).map { it.uri } | |
| val combinedUris = mutableListOf<String>() | |
| var cursor: String? = null | |
| do { | |
| val request = if (cursor == null) { | |
| ListResourcesRequest() | |
| } else { | |
| ListResourcesRequest(PaginatedRequestParams(cursor = cursor)) | |
| } | |
| val response = client.listResources(request) | |
| combinedUris += response.resources.map { it.uri } | |
| cursor = response.nextCursor | |
| } while (cursor != null) | |
| assertTrue(combinedUris.isNotEmpty()) |
| val exception = org.junit.jupiter.api.assertThrows<McpException> { | ||
| runBlocking { | ||
| client.listResources(ListResourcesRequest(PaginatedRequestParams(cursor = "bad"))) | ||
| } |
There was a problem hiding this comment.
This test runs in runBlocking(Dispatchers.IO) already; the additional nested runBlocking { client.listResources(...) } inside assertThrows is unnecessary. Consider using a suspending assertion within the current runBlocking scope, or remove the outer runBlocking and keep a single runBlocking around the suspending call.
| val nextCursor = first.nextCursor | ||
| assertNotNull(nextCursor) | ||
|
|
||
| val second = client.listPrompts(ListPromptsRequest(PaginatedRequestParams(cursor = nextCursor))) | ||
| assertTrue(second.prompts.isNotEmpty()) | ||
|
|
||
| val combined = first.prompts + second.prompts | ||
| assertTrue(combined.any { it.name.startsWith(pagePrefix) }) |
There was a problem hiding this comment.
all is built from server.prompts.values (persistent map iteration order is not guaranteed). Because the test only fetches two pages, the added paginated-prompt-* prompts may not be present in first + second, making this test potentially flaky. Sort/filter the prompts list (e.g., by name) and/or iterate through pages until nextCursor == null before asserting.
| val nextCursor = first.nextCursor | |
| assertNotNull(nextCursor) | |
| val second = client.listPrompts(ListPromptsRequest(PaginatedRequestParams(cursor = nextCursor))) | |
| assertTrue(second.prompts.isNotEmpty()) | |
| val combined = first.prompts + second.prompts | |
| assertTrue(combined.any { it.name.startsWith(pagePrefix) }) | |
| val allPrompts = mutableListOf<io.modelcontextprotocol.kotlin.sdk.types.Prompt>() | |
| allPrompts.addAll(first.prompts) | |
| var cursor = first.nextCursor | |
| while (cursor != null) { | |
| val page = client.listPrompts(ListPromptsRequest(PaginatedRequestParams(cursor = cursor))) | |
| allPrompts.addAll(page.prompts) | |
| cursor = page.nextCursor | |
| } | |
| assertTrue(allPrompts.any { it.name.startsWith(pagePrefix) }) |
| val exception = org.junit.jupiter.api.assertThrows<McpException> { | ||
| runBlocking { | ||
| client.listPrompts(ListPromptsRequest(PaginatedRequestParams(cursor = "not-a-number"))) | ||
| } |
There was a problem hiding this comment.
This test already uses runBlocking(Dispatchers.IO); the nested runBlocking { client.listPrompts(...) } inside assertThrows is redundant. Prefer a single runBlocking (either outer or inner) and a suspending assertion so the test structure is simpler and avoids blocking an extra thread.
| } | ||
|
|
||
| server.addTool(name = "test-logging-level", description = "test") { request -> | ||
| LoggingLevel.values().forEach { level -> |
There was a problem hiding this comment.
LoggingLevel.values() allocates a new array each call; elsewhere in the codebase tests use LoggingLevel.entries, which is allocation-free and the preferred Kotlin API. Consider switching to entries for consistency and to avoid unnecessary allocations.
| LoggingLevel.values().forEach { level -> | |
| LoggingLevel.entries.forEach { level -> |
6735788 to
af97f37
Compare
84d33e7 to
d839b42
Compare
|
fa786e1 to
45a3348
Compare
...rotocol/kotlin/sdk/integration/kotlin/streamablehttp/LoggingIntegrationTestStreamableHttp.kt
Outdated
Show resolved
Hide resolved
...rotocol/kotlin/sdk/integration/kotlin/streamablehttp/LoggingIntegrationTestStreamableHttp.kt
Outdated
Show resolved
Hide resolved
...rotocol/kotlin/sdk/integration/kotlin/streamablehttp/LoggingIntegrationTestStreamableHttp.kt
Outdated
Show resolved
Hide resolved
...rotocol/kotlin/sdk/integration/kotlin/streamablehttp/LoggingIntegrationTestStreamableHttp.kt
Outdated
Show resolved
Hide resolved
...lin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt
Outdated
Show resolved
Hide resolved
.../kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt
Outdated
Show resolved
Hide resolved
...otlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt
Outdated
Show resolved
Hide resolved
...otlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt
Outdated
Show resolved
Hide resolved
45a3348 to
24a1bcc
Compare
…able 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)
24a1bcc to
dfa245f
Compare
This PR adds the remaining non-security integration tests for the Streamable HTTP transport as outlined in #183 and left over from #486.
Changes included:
Abstract*IntegrationTest.McpExceptionwithINTERNAL_ERROR.LoggingIntegrationTestStreamableHttpto specifically testsetLeveland logging message notifications over Streamable HTTP.Note: Security (Allow/Deny) tests are intentionally excluded as discussed in #486, pending further server auth model implementation.