Server feature list changed and resource updated notifications (#441)
<!-- Provide a brief summary of your changes --> ## Motivation and Context <!-- Why is this change needed? What problem does it solve? --> Implemented thread-safe notification service following eventual consistency principle. Fixes https://github.com/modelcontextprotocol/kotlin-sdk/issues/249 Contracts: * If the client connection and feature update happened in the same thread, the timestamps of the events will be consequent ``` server.connect(client) server.add(feature) // client will receive the notification ``` ``` server.add(feature) server.connect(client) // client will NOT receive the notification ``` * If the client connection and feature update happened in the different threads, both options can be possible The flow of the notification is explained in the diagram: <img width="663" height="413" alt="image" src="https://github.com/user-attachments/assets/e2c4c108-53dd-408d-97ef-97110ce5c979" /> Documentation references: https://modelcontextprotocol.io/specification/2025-06-18/server/prompts#list-changed-notification https://modelcontextprotocol.io/specification/2025-06-18/server/tools#list-changed-notification https://modelcontextprotocol.io/specification/2025-06-18/server/resources#list-changed-notification https://modelcontextprotocol.io/specification/2025-06-18/server/resources#subscriptions Questions: * I haven't found the subscription confirmed response message content docs, how it should look like? ## How Has This Been Tested? <!-- Have you tested this in a real application? Which scenarios were tested? --> Multiple tests with various notification scenarios were added ## Breaking Changes <!-- Will users need to update their code or configurations? --> ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [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 - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions -->
This commit is contained in:
parent
cc9b50ff76
commit
4645ebfd17
8 changed files with 1307 additions and 60 deletions
|
|
@ -1,8 +1,14 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.BaseNotificationParams
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotificationParams
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.toJSON
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.BeforeTest
|
||||
|
|
@ -107,4 +113,94 @@ class InMemoryTransportTest {
|
|||
serverTransport.start()
|
||||
assertEquals(message, receivedMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should send ToolListChangedNotification from server to client`() = runTest {
|
||||
val notification = ToolListChangedNotification(
|
||||
BaseNotificationParams(),
|
||||
)
|
||||
|
||||
var receivedMessage: JSONRPCMessage? = null
|
||||
clientTransport.onMessage { msg ->
|
||||
receivedMessage = msg
|
||||
}
|
||||
|
||||
val rpcMessage = notification.toJSON()
|
||||
serverTransport.send(rpcMessage)
|
||||
assertEquals(rpcMessage, receivedMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should send PromptListChangedNotification from server to client`() = runTest {
|
||||
val notification = PromptListChangedNotification(
|
||||
BaseNotificationParams(),
|
||||
)
|
||||
|
||||
var receivedMessage: JSONRPCMessage? = null
|
||||
clientTransport.onMessage { msg ->
|
||||
receivedMessage = msg
|
||||
}
|
||||
|
||||
val rpcMessage = notification.toJSON()
|
||||
serverTransport.send(rpcMessage)
|
||||
assertEquals(rpcMessage, receivedMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should send ResourceListChangedNotification from server to client`() = runTest {
|
||||
val notification = ResourceListChangedNotification(
|
||||
BaseNotificationParams(),
|
||||
)
|
||||
|
||||
var receivedMessage: JSONRPCMessage? = null
|
||||
clientTransport.onMessage { msg ->
|
||||
receivedMessage = msg
|
||||
}
|
||||
|
||||
val rpcMessage = notification.toJSON()
|
||||
serverTransport.send(rpcMessage)
|
||||
assertEquals(rpcMessage, receivedMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should send ResourceUpdatedNotification from server to client`() = runTest {
|
||||
val notification = ResourceUpdatedNotification(
|
||||
ResourceUpdatedNotificationParams(
|
||||
uri = "file:///workspace/data.json",
|
||||
),
|
||||
)
|
||||
|
||||
var receivedMessage: JSONRPCMessage? = null
|
||||
clientTransport.onMessage { msg ->
|
||||
receivedMessage = msg
|
||||
}
|
||||
|
||||
val rpcMessage = notification.toJSON()
|
||||
serverTransport.send(rpcMessage)
|
||||
assertEquals(rpcMessage, receivedMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle multiple notifications in sequence`() = runTest {
|
||||
val notifications = listOf(
|
||||
ToolListChangedNotification(),
|
||||
PromptListChangedNotification(),
|
||||
ResourceListChangedNotification(),
|
||||
ResourceUpdatedNotification(ResourceUpdatedNotificationParams(uri = "file:///workspace/data.json")),
|
||||
)
|
||||
|
||||
val receivedMessages = mutableListOf<JSONRPCMessage>()
|
||||
clientTransport.onMessage { msg ->
|
||||
receivedMessages.add(msg)
|
||||
}
|
||||
|
||||
notifications.forEach { notification ->
|
||||
serverTransport.send(notification.toJSON())
|
||||
}
|
||||
|
||||
assertEquals(notifications.size, receivedMessages.size)
|
||||
notifications.forEachIndexed { index, notification ->
|
||||
assertEquals(notification.toJSON(), receivedMessages[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.Method
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.Prompt
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.awaitility.kotlin.await
|
||||
import org.awaitility.kotlin.untilAsserted
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ServerPromptsNotificationTest : AbstractServerFeaturesTest() {
|
||||
|
||||
override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities(
|
||||
prompts = ServerCapabilities.Prompts(true),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `addPrompt should send notification`() = runTest {
|
||||
// Configure notification handler
|
||||
var promptListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<PromptListChangedNotification>(Method.Defined.NotificationsPromptsListChanged) {
|
||||
promptListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Add a prompt
|
||||
val testPrompt = Prompt("test-prompt", "Test Prompt", null)
|
||||
server.addPrompt(testPrompt) {
|
||||
GetPromptResult(
|
||||
description = "Test prompt description",
|
||||
messages = listOf(),
|
||||
)
|
||||
}
|
||||
|
||||
// Remove the prompt
|
||||
val result = server.removePrompt(testPrompt.name)
|
||||
|
||||
// Verify the prompt was removed
|
||||
assertTrue(result, "Prompt should be removed successfully")
|
||||
|
||||
// Verify that the notification was sent
|
||||
await untilAsserted {
|
||||
assertTrue(promptListChangedNotificationReceived, "Notification should be sent when prompt is added")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removePrompts should remove multiple prompts and send two notifications`() = runTest {
|
||||
// Configure notification handler
|
||||
var promptListChangedNotificationReceivedCount = 0
|
||||
client.setNotificationHandler<PromptListChangedNotification>(Method.Defined.NotificationsPromptsListChanged) {
|
||||
promptListChangedNotificationReceivedCount += 1
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Add prompts
|
||||
val testPrompt1 = Prompt("test-prompt-1", "Test Prompt 1", null)
|
||||
val testPrompt2 = Prompt("test-prompt-2", "Test Prompt 2", null)
|
||||
|
||||
server.addPrompt(testPrompt1) {
|
||||
GetPromptResult(
|
||||
description = "Test prompt description 1",
|
||||
messages = listOf(),
|
||||
)
|
||||
}
|
||||
server.addPrompt(testPrompt2) {
|
||||
GetPromptResult(
|
||||
description = "Test prompt description 2",
|
||||
messages = listOf(),
|
||||
)
|
||||
}
|
||||
|
||||
// Remove the prompts
|
||||
val result = server.removePrompts(listOf(testPrompt1.name, testPrompt2.name))
|
||||
|
||||
// Verify the prompts were removed
|
||||
assertEquals(2, result, "Both prompts should be removed")
|
||||
|
||||
// Verify that the notifications were sent twice
|
||||
await untilAsserted {
|
||||
assertEquals(
|
||||
4,
|
||||
promptListChangedNotificationReceivedCount,
|
||||
"Two notifications should be sent when prompts are added and two when removed",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `notification should not be send when removed prompt does not exists`() = runTest {
|
||||
// Track notifications
|
||||
var promptListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<PromptListChangedNotification>(Method.Defined.NotificationsPromptsListChanged) {
|
||||
promptListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Try to remove a non-existent prompt
|
||||
val result = server.removePrompt("non-existent-prompt")
|
||||
|
||||
// Verify the result
|
||||
assertFalse(result, "Removing non-existent prompt should return false")
|
||||
await untilAsserted {
|
||||
assertFalse(
|
||||
promptListChangedNotificationReceived,
|
||||
"No notification should be sent when prompt doesn't exist",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,11 +17,18 @@ import kotlin.test.assertTrue
|
|||
class ServerPromptsTest : AbstractServerFeaturesTest() {
|
||||
|
||||
override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities(
|
||||
prompts = ServerCapabilities.Prompts(false),
|
||||
prompts = ServerCapabilities.Prompts(null),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `removePrompt should remove a prompt`() = runTest {
|
||||
fun `removePrompt should remove a prompt and do not send notification`() = runTest {
|
||||
// Configure notification handler
|
||||
var promptListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<PromptListChangedNotification>(Method.Defined.NotificationsPromptsListChanged) {
|
||||
promptListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Add a prompt
|
||||
val testPrompt = Prompt("test-prompt", "Test Prompt", null)
|
||||
server.addPrompt(testPrompt) {
|
||||
|
|
@ -36,13 +43,30 @@ class ServerPromptsTest : AbstractServerFeaturesTest() {
|
|||
|
||||
// Verify the prompt was removed
|
||||
assertTrue(result, "Prompt should be removed successfully")
|
||||
|
||||
// Verify that the notification was not sent
|
||||
assertFalse(
|
||||
promptListChangedNotificationReceived,
|
||||
"No notification should be sent when prompts capability is not supported",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removePrompts should remove multiple prompts and send notification`() = runTest {
|
||||
fun `removePrompts should remove multiple prompts`() = runTest {
|
||||
// Configure notification handler
|
||||
var promptListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<PromptListChangedNotification>(Method.Defined.NotificationsPromptsListChanged) {
|
||||
promptListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Add prompts
|
||||
val testPrompt1 = Prompt("test-prompt-1", "Test Prompt 1", null)
|
||||
val testPrompt2 = Prompt("test-prompt-2", "Test Prompt 2", null)
|
||||
client.setNotificationHandler<PromptListChangedNotification>(Method.Defined.NotificationsPromptsListChanged) {
|
||||
throw IllegalStateException("Notification should not be sent")
|
||||
}
|
||||
|
||||
server.addPrompt(testPrompt1) {
|
||||
GetPromptResult(
|
||||
description = "Test prompt description 1",
|
||||
|
|
@ -61,6 +85,12 @@ class ServerPromptsTest : AbstractServerFeaturesTest() {
|
|||
|
||||
// Verify the prompts were removed
|
||||
assertEquals(2, result, "Both prompts should be removed")
|
||||
|
||||
// Verify that the notification was not sent
|
||||
assertFalse(
|
||||
promptListChangedNotificationReceived,
|
||||
"No notification should be sent when prompts capability is not supported",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -82,6 +112,12 @@ class ServerPromptsTest : AbstractServerFeaturesTest() {
|
|||
|
||||
@Test
|
||||
fun `removePrompt should throw when prompts capability is not supported`() = runTest {
|
||||
var promptListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<PromptListChangedNotification>(Method.Defined.NotificationsPromptsListChanged) {
|
||||
promptListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Create server without prompts capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(),
|
||||
|
|
@ -96,5 +132,11 @@ class ServerPromptsTest : AbstractServerFeaturesTest() {
|
|||
server.removePrompt("test-prompt")
|
||||
}
|
||||
assertEquals("Server does not support prompts capability.", exception.message)
|
||||
|
||||
// Verify that the notification was not sent
|
||||
assertFalse(
|
||||
promptListChangedNotificationReceived,
|
||||
"No notification should be sent when prompts capability is not supported",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,186 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.Method
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequestParams
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.awaitility.kotlin.await
|
||||
import org.awaitility.kotlin.untilAsserted
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ServerResourcesNotificationSubscribeTest : AbstractServerFeaturesTest() {
|
||||
|
||||
override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities(
|
||||
resources = ServerCapabilities.Resources(null, true),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `removeResource should send resource update notification`() = runTest {
|
||||
val notifications = mutableListOf<ResourceUpdatedNotification>()
|
||||
client.setNotificationHandler<ResourceUpdatedNotification>(Method.Defined.NotificationsResourcesUpdated) {
|
||||
notifications.add(it)
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Add resources
|
||||
val testResourceUri1 = "test://resource1"
|
||||
val testResourceUri2 = "test://resource2"
|
||||
|
||||
server.addResource(
|
||||
uri = testResourceUri1,
|
||||
name = "Test Resource 1",
|
||||
description = "A test resource 1",
|
||||
mimeType = "text/plain",
|
||||
) {
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = "Test resource content 1",
|
||||
uri = testResourceUri1,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
client.subscribeResource(SubscribeRequest(SubscribeRequestParams(uri = testResourceUri1)))
|
||||
|
||||
server.addResource(
|
||||
uri = testResourceUri2,
|
||||
name = "Test Resource 2",
|
||||
description = "A test resource 2",
|
||||
mimeType = "text/plain",
|
||||
) {
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = "Test resource content 2",
|
||||
uri = testResourceUri2,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Remove the resource
|
||||
val result = server.removeResource(testResourceUri1)
|
||||
|
||||
// Verify the resource was removed
|
||||
assertTrue(result, "Resource should be removed successfully")
|
||||
|
||||
// Verify that the notification was sent
|
||||
await untilAsserted {
|
||||
assertEquals(1, notifications.size, "Notification should be sent when resource 1 was deleted")
|
||||
}
|
||||
assertEquals(testResourceUri1, notifications[0].params.uri, "Notification should contain the resource 1 URI")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeResource for two resources should send two separate notifications`() = runTest {
|
||||
val notifications = mutableListOf<ResourceUpdatedNotification>()
|
||||
client.setNotificationHandler<ResourceUpdatedNotification>(Method.Defined.NotificationsResourcesUpdated) {
|
||||
notifications.add(it)
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Add resources
|
||||
val testResourceUri1 = "test://resource1"
|
||||
val testResourceUri2 = "test://resource2"
|
||||
|
||||
server.addResource(
|
||||
uri = testResourceUri1,
|
||||
name = "Test Resource 1",
|
||||
description = "A test resource 1",
|
||||
mimeType = "text/plain",
|
||||
) {
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = "Test resource content 1",
|
||||
uri = testResourceUri1,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
server.addResource(
|
||||
uri = testResourceUri2,
|
||||
name = "Test Resource 2",
|
||||
description = "A test resource 2",
|
||||
mimeType = "text/plain",
|
||||
) {
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = "Test resource content 2",
|
||||
uri = testResourceUri2,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
client.subscribeResource(SubscribeRequest(SubscribeRequestParams(uri = testResourceUri1)))
|
||||
client.subscribeResource(SubscribeRequest(SubscribeRequestParams(uri = testResourceUri2)))
|
||||
|
||||
// Remove the resource
|
||||
val result1 = server.removeResource(testResourceUri1)
|
||||
val result2 = server.removeResource(testResourceUri2)
|
||||
|
||||
// Verify the resource was removed
|
||||
assertTrue(result1, "Resource 1 should be removed successfully")
|
||||
assertTrue(result2, "Resource 2 should be removed successfully")
|
||||
|
||||
println(notifications.map { it.params.uri })
|
||||
// Verify that the notification was sent
|
||||
await untilAsserted {
|
||||
assertEquals(
|
||||
2,
|
||||
notifications.size,
|
||||
"Notification should be sent when resource 1 and resource 2 was deleted",
|
||||
)
|
||||
}
|
||||
|
||||
val deletedResources = listOf(notifications[0].params.uri, notifications[1].params.uri)
|
||||
assertTrue(
|
||||
deletedResources.contains(testResourceUri1),
|
||||
"Notification should contain the removed resource 1 URI",
|
||||
)
|
||||
assertTrue(
|
||||
deletedResources.contains(testResourceUri2),
|
||||
"Notification should contain the removed resource 2 URI",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `notification should not be send when removed resource does not exists`() = runTest {
|
||||
// Track notifications
|
||||
var resourceListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<ResourceListChangedNotification>(
|
||||
Method.Defined.NotificationsResourcesListChanged,
|
||||
) {
|
||||
resourceListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Try to remove a non-existent resource
|
||||
val result = server.removeResource("non-existent-resource")
|
||||
|
||||
// Verify the result
|
||||
assertFalse(result, "Removing non-existent resource should return false")
|
||||
assertFalse(
|
||||
resourceListChangedNotificationReceived,
|
||||
"No notification should be sent when resource doesn't exist",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.Method
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.awaitility.kotlin.await
|
||||
import org.awaitility.kotlin.untilAsserted
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ServerResourcesNotificationTest : AbstractServerFeaturesTest() {
|
||||
|
||||
override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities(
|
||||
resources = ServerCapabilities.Resources(true, null),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `addResource should send notification`() = runTest {
|
||||
// Configure notification handler
|
||||
var resourceListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<ResourceListChangedNotification>(
|
||||
Method.Defined.NotificationsResourcesListChanged,
|
||||
) {
|
||||
resourceListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Add a resource
|
||||
val testResourceUri = "test://resource"
|
||||
server.addResource(
|
||||
uri = testResourceUri,
|
||||
name = "Test Resource",
|
||||
description = "A test resource",
|
||||
mimeType = "text/plain",
|
||||
) {
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = "Test resource content",
|
||||
uri = testResourceUri,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Remove the resource
|
||||
val result = server.removeResource(testResourceUri)
|
||||
|
||||
// Verify the resource was removed
|
||||
assertTrue(result, "Resource should be removed successfully")
|
||||
|
||||
// Verify that the notification was sent
|
||||
await untilAsserted {
|
||||
assertTrue(resourceListChangedNotificationReceived, "Notification should be sent when resource is added")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeResources should remove multiple resources and send two notifications`() = runTest {
|
||||
// Configure notification handler
|
||||
var resourceListChangedNotificationReceivedCount = 0
|
||||
client.setNotificationHandler<ResourceListChangedNotification>(
|
||||
Method.Defined.NotificationsResourcesListChanged,
|
||||
) {
|
||||
resourceListChangedNotificationReceivedCount += 1
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Add resources
|
||||
val testResourceUri1 = "test://resource1"
|
||||
val testResourceUri2 = "test://resource2"
|
||||
|
||||
server.addResource(
|
||||
uri = testResourceUri1,
|
||||
name = "Test Resource 1",
|
||||
description = "A test resource 1",
|
||||
mimeType = "text/plain",
|
||||
) {
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = "Test resource content 1",
|
||||
uri = testResourceUri1,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
server.addResource(
|
||||
uri = testResourceUri2,
|
||||
name = "Test Resource 2",
|
||||
description = "A test resource 2",
|
||||
mimeType = "text/plain",
|
||||
) {
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = "Test resource content 2",
|
||||
uri = testResourceUri2,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Remove the resources
|
||||
val result = server.removeResources(listOf(testResourceUri1, testResourceUri2))
|
||||
|
||||
// Verify the resources were removed
|
||||
assertEquals(2, result, "Both resources should be removed")
|
||||
|
||||
// Verify that the notifications were sent twice
|
||||
await untilAsserted {
|
||||
assertEquals(
|
||||
4,
|
||||
resourceListChangedNotificationReceivedCount,
|
||||
"Two notifications should be sent when resources are added and two when removed",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `notification should not be send when removed resource does not exists`() = runTest {
|
||||
// Track notifications
|
||||
var resourceListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<ResourceListChangedNotification>(
|
||||
Method.Defined.NotificationsResourcesListChanged,
|
||||
) {
|
||||
resourceListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Try to remove a non-existent resource
|
||||
val result = server.removeResource("non-existent-resource")
|
||||
|
||||
// Verify the result
|
||||
assertFalse(result, "Removing non-existent resource should return false")
|
||||
assertFalse(
|
||||
resourceListChangedNotificationReceived,
|
||||
"No notification should be sent when resource doesn't exist",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ class ServerResourcesTest : AbstractServerFeaturesTest() {
|
|||
)
|
||||
|
||||
@Test
|
||||
fun `removeResource should remove a resource and send notification`() = runTest {
|
||||
fun `removeResource should remove a resource`() = runTest {
|
||||
// Add a resource
|
||||
val testResourceUri = "test://resource"
|
||||
server.addResource(
|
||||
|
|
@ -49,7 +49,7 @@ class ServerResourcesTest : AbstractServerFeaturesTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `removeResources should remove multiple resources and send notification`() = runTest {
|
||||
fun `removeResources should remove multiple resources`() = runTest {
|
||||
// Add resources
|
||||
val testResourceUri1 = "test://resource1"
|
||||
val testResourceUri2 = "test://resource2"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.Method
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.awaitility.kotlin.await
|
||||
import org.awaitility.kotlin.untilAsserted
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ServerToolsNotificationTest : AbstractServerFeaturesTest() {
|
||||
|
||||
override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities(
|
||||
tools = ServerCapabilities.Tools(true),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `addTool should send notification`() = runTest {
|
||||
// Configure notification handler
|
||||
var toolListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<ToolListChangedNotification>(Method.Defined.NotificationsToolsListChanged) {
|
||||
toolListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Add a tool
|
||||
server.addTool("test-tool", "Test Tool", ToolSchema()) {
|
||||
CallToolResult(listOf(TextContent("Test result")))
|
||||
}
|
||||
|
||||
// Remove the tool
|
||||
val result = server.removeTool("test-tool")
|
||||
|
||||
// Verify the tool was removed
|
||||
assertTrue(result, "Tool should be removed successfully")
|
||||
|
||||
// Verify that the notification was sent
|
||||
await untilAsserted {
|
||||
assertTrue(toolListChangedNotificationReceived, "Notification should be sent when tool is added")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeTools should remove multiple tools and send two notifications`() = runTest {
|
||||
// Configure notification handler
|
||||
var toolListChangedNotificationReceivedCount = 0
|
||||
client.setNotificationHandler<ToolListChangedNotification>(Method.Defined.NotificationsToolsListChanged) {
|
||||
toolListChangedNotificationReceivedCount += 1
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Add tools
|
||||
server.addTool("test-tool-1", "Test Tool 1") {
|
||||
CallToolResult(listOf(TextContent("Test result 1")))
|
||||
}
|
||||
server.addTool("test-tool-2", "Test Tool 2") {
|
||||
CallToolResult(listOf(TextContent("Test result 2")))
|
||||
}
|
||||
|
||||
// Remove the tools
|
||||
val result = server.removeTools(listOf("test-tool-1", "test-tool-2"))
|
||||
// Verify the tools were removed
|
||||
assertEquals(2, result, "Both tools should be removed")
|
||||
|
||||
// Verify that the notifications were sent twice
|
||||
await untilAsserted {
|
||||
assertEquals(
|
||||
4,
|
||||
toolListChangedNotificationReceivedCount,
|
||||
"Two notifications should be sent when tools are added and two when removed",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `notification should not be send when removed tool does not exists`() = runTest {
|
||||
// Track notifications
|
||||
var toolListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<ToolListChangedNotification>(Method.Defined.NotificationsToolsListChanged) {
|
||||
toolListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Try to remove a non-existent tool
|
||||
val result = server.removeTool("non-existent-tool")
|
||||
// Close the server to stop processing further events and flush notifications
|
||||
server.close()
|
||||
|
||||
// Verify the result
|
||||
assertFalse(result, "Removing non-existent tool should return false")
|
||||
assertFalse(toolListChangedNotificationReceived, "No notification should be sent when tool doesn't exist")
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,14 @@ class ServerToolsTest : AbstractServerFeaturesTest() {
|
|||
)
|
||||
|
||||
@Test
|
||||
fun `removeTool should remove a tool`() = runTest {
|
||||
fun `removeTool should remove a tool and do not send notification`() = runTest {
|
||||
// Configure notification handler
|
||||
var toolListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<ToolListChangedNotification>(Method.Defined.NotificationsToolsListChanged) {
|
||||
toolListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Add a tool
|
||||
server.addTool("test-tool", "Test Tool", ToolSchema()) {
|
||||
CallToolResult(listOf(TextContent("Test result")))
|
||||
|
|
@ -33,6 +40,45 @@ class ServerToolsTest : AbstractServerFeaturesTest() {
|
|||
|
||||
// Verify the tool was removed
|
||||
assertTrue(result, "Tool should be removed successfully")
|
||||
|
||||
// Verify that the notification was not sent
|
||||
assertFalse(
|
||||
toolListChangedNotificationReceived,
|
||||
"No notification should be sent when tools capability is not supported",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeTools should remove multiple tools`() = runTest {
|
||||
// Configure notification handler
|
||||
var toolListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<ToolListChangedNotification>(Method.Defined.NotificationsToolsListChanged) {
|
||||
toolListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Add tools
|
||||
server.addTool("test-tool-1", "Test Tool 1") {
|
||||
CallToolResult(listOf(TextContent("Test result 1")))
|
||||
}
|
||||
server.addTool("test-tool-2", "Test Tool 2") {
|
||||
CallToolResult(listOf(TextContent("Test result 2")))
|
||||
}
|
||||
client.setNotificationHandler<ToolListChangedNotification>(Method.Defined.NotificationsToolsListChanged) {
|
||||
throw IllegalStateException("Notification should not be sent")
|
||||
}
|
||||
|
||||
// Remove the tools
|
||||
val result = server.removeTools(listOf("test-tool-1", "test-tool-2"))
|
||||
|
||||
// Verify the tools were removed
|
||||
assertEquals(2, result, "Both tools should be removed")
|
||||
|
||||
// Verify that the notification was not sent
|
||||
assertFalse(
|
||||
toolListChangedNotificationReceived,
|
||||
"No notification should be sent when tools capability is not supported",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -54,6 +100,12 @@ class ServerToolsTest : AbstractServerFeaturesTest() {
|
|||
|
||||
@Test
|
||||
fun `removeTool should throw when tools capability is not supported`() = runTest {
|
||||
var toolListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<ToolListChangedNotification>(Method.Defined.NotificationsToolsListChanged) {
|
||||
toolListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Create server without tools capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(),
|
||||
|
|
@ -68,22 +120,11 @@ class ServerToolsTest : AbstractServerFeaturesTest() {
|
|||
server.removeTool("test-tool")
|
||||
}
|
||||
assertEquals("Server does not support tools capability.", exception.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeTools should remove multiple tools`() = runTest {
|
||||
// Add tools
|
||||
server.addTool("test-tool-1", "Test Tool 1") {
|
||||
CallToolResult(listOf(TextContent("Test result 1")))
|
||||
}
|
||||
server.addTool("test-tool-2", "Test Tool 2") {
|
||||
CallToolResult(listOf(TextContent("Test result 2")))
|
||||
}
|
||||
|
||||
// Remove the tools
|
||||
val result = server.removeTools(listOf("test-tool-1", "test-tool-2"))
|
||||
|
||||
// Verify the tools were removed
|
||||
assertEquals(2, result, "Both tools should be removed")
|
||||
// Verify that the notification was not sent
|
||||
assertFalse(
|
||||
toolListChangedNotificationReceived,
|
||||
"No notification should be sent when tools capability is not supported",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue