diff --git a/api/kotlin-sdk.api b/api/kotlin-sdk.api index 4f0b20c..4154809 100644 --- a/api/kotlin-sdk.api +++ b/api/kotlin-sdk.api @@ -2794,6 +2794,12 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server : io/modelcontextp public final fun onClose (Lkotlin/jvm/functions/Function0;)V public final fun onInitalized (Lkotlin/jvm/functions/Function0;)V public final fun ping (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun removePrompt (Ljava/lang/String;)Z + public final fun removePrompts (Ljava/util/List;)I + public final fun removeResource (Ljava/lang/String;)Z + public final fun removeResources (Ljava/util/List;)I + public final fun removeTool (Ljava/lang/String;)Z + public final fun removeTools (Ljava/util/List;)I public final fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun sendPromptListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun sendResourceListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index e24419e..ae622d2 100644 --- a/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -173,6 +173,60 @@ public open class Server( } } + /** + * Removes a single tool by name. + * + * @param name The name of the tool to remove. + * @return True if the tool was removed, false if it wasn't found. + * @throws IllegalStateException If the server does not support tools. + */ + public fun removeTool(name: String): Boolean { + if (capabilities.tools == null) { + logger.error { "Failed to remove tool '$name': Server does not support tools capability" } + throw IllegalStateException("Server does not support tools capability.") + } + logger.info { "Removing tool: $name" } + val removed = tools.remove(name) != null + logger.debug { + if (removed) { + "Tool removed: $name" + } else { + "Tool not found: $name" + } + } + return removed + } + + /** + * Removes multiple tools at once. + * + * @param toolNames A list of tool names to remove. + * @return The number of tools that were successfully removed. + * @throws IllegalStateException If the server does not support tools. + */ + public fun removeTools(toolNames: List): Int { + if (capabilities.tools == null) { + logger.error { "Failed to remove tools: Server does not support tools capability" } + throw IllegalStateException("Server does not support tools capability.") + } + logger.info { "Removing ${toolNames.size} tools" } + var removedCount = 0 + for (name in toolNames) { + logger.debug { "Removing tool: $name" } + if (tools.remove(name) != null) { + removedCount++ + } + } + logger.info { + if (removedCount > 0) { + "Removed $removedCount tools" + } else { + "No tools were removed" + } + } + return removedCount + } + /** * Registers a single prompt. The prompt can then be retrieved by the client. * @@ -226,6 +280,60 @@ public open class Server( } } + /** + * Removes a single prompt by name. + * + * @param name The name of the prompt to remove. + * @return True if the prompt was removed, false if it wasn't found. + * @throws IllegalStateException If the server does not support prompts. + */ + public fun removePrompt(name: String): Boolean { + if (capabilities.prompts == null) { + logger.error { "Failed to remove prompt '$name': Server does not support prompts capability" } + throw IllegalStateException("Server does not support prompts capability.") + } + logger.info { "Removing prompt: $name" } + val removed = prompts.remove(name) != null + logger.debug { + if (removed) { + "Prompt removed: $name" + } else { + "Prompt not found: $name" + } + } + return removed + } + + /** + * Removes multiple prompts at once. + * + * @param promptNames A list of prompt names to remove. + * @return The number of prompts that were successfully removed. + * @throws IllegalStateException If the server does not support prompts. + */ + public fun removePrompts(promptNames: List): Int { + if (capabilities.prompts == null) { + logger.error { "Failed to remove prompts: Server does not support prompts capability" } + throw IllegalStateException("Server does not support prompts capability.") + } + logger.info { "Removing ${promptNames.size} prompts" } + var removedCount = 0 + for (name in promptNames) { + logger.debug { "Removing prompt: $name" } + if (prompts.remove(name) != null) { + removedCount++ + } + } + logger.info { + if (removedCount > 0) { + "Removed $removedCount prompts" + } else { + "No prompts were removed" + } + } + return removedCount + } + /** * Registers a single resource. The resource content can then be read by the client. * @@ -269,6 +377,60 @@ public open class Server( } } + /** + * Removes a single resource by URI. + * + * @param uri The URI of the resource to remove. + * @return True if the resource was removed, false if it wasn't found. + * @throws IllegalStateException If the server does not support resources. + */ + public fun removeResource(uri: String): Boolean { + if (capabilities.resources == null) { + logger.error { "Failed to remove resource '$uri': Server does not support resources capability" } + throw IllegalStateException("Server does not support resources capability.") + } + logger.info { "Removing resource: $uri" } + val removed = resources.remove(uri) != null + logger.debug { + if (removed) { + "Resource removed: $uri" + } else { + "Resource not found: $uri" + } + } + return removed + } + + /** + * Removes multiple resources at once. + * + * @param uris A list of resource URIs to remove. + * @return The number of resources that were successfully removed. + * @throws IllegalStateException If the server does not support resources. + */ + public fun removeResources(uris: List): Int { + if (capabilities.resources == null) { + logger.error { "Failed to remove resources: Server does not support resources capability" } + throw IllegalStateException("Server does not support resources capability.") + } + logger.info { "Removing ${uris.size} resources" } + var removedCount = 0 + for (uri in uris) { + logger.debug { "Removing resource: $uri" } + if (resources.remove(uri) != null) { + removedCount++ + } + } + logger.info { + if (removedCount > 0) { + "Removed $removedCount resources" + } else { + "No resources were removed" + } + } + return removedCount + } + /** * Sends a ping request to the client to check connectivity. * diff --git a/src/jvmTest/kotlin/server/ServerTest.kt b/src/jvmTest/kotlin/server/ServerTest.kt new file mode 100644 index 0000000..10294b4 --- /dev/null +++ b/src/jvmTest/kotlin/server/ServerTest.kt @@ -0,0 +1,484 @@ +package server + +import io.modelcontextprotocol.kotlin.sdk.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.EmptyJsonObject +import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.Implementation +import io.modelcontextprotocol.kotlin.sdk.Method +import io.modelcontextprotocol.kotlin.sdk.Prompt +import io.modelcontextprotocol.kotlin.sdk.PromptListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult +import io.modelcontextprotocol.kotlin.sdk.Resource +import io.modelcontextprotocol.kotlin.sdk.ResourceListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.Tool +import io.modelcontextprotocol.kotlin.sdk.ToolListChangedNotification +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.server.RegisteredPrompt +import io.modelcontextprotocol.kotlin.sdk.server.RegisteredResource +import io.modelcontextprotocol.kotlin.sdk.server.RegisteredTool +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.PromptMessage +import io.modelcontextprotocol.kotlin.sdk.PromptMessageContent +import io.modelcontextprotocol.kotlin.sdk.Role +import io.modelcontextprotocol.kotlin.sdk.TextContent +import io.modelcontextprotocol.kotlin.sdk.TextResourceContents +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ServerTest { + + @Test + fun `removeTool should remove a tool`() = runTest { + // Create server with tools capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(null) + ) + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions + ) + + // Add a tool + server.addTool("test-tool", "Test Tool", Tool.Input()) { request -> + CallToolResult(listOf(TextContent("Test result"))) + } + + // Setup client + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + ) + + // Connect client and server + launch { client.connect(clientTransport) } + launch { server.connect(serverTransport) } + + // Remove the tool + val result = server.removeTool("test-tool") + + // Verify the tool was removed + assertTrue(result, "Tool should be removed successfully") + } + + @Test + fun `removeTool should return false when tool does not exist`() = runTest { + // Create server with tools capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(null) + ) + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions + ) + + // Setup client + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + ) + + // Track notifications + var toolListChangedNotificationReceived = false + client.setNotificationHandler(Method.Defined.NotificationsToolsListChanged) { + toolListChangedNotificationReceived = true + CompletableDeferred(Unit) + } + + // Connect client and server + launch { client.connect(clientTransport) } + launch { server.connect(serverTransport) } + + // Try to remove a non-existent tool + val result = server.removeTool("non-existent-tool") + + // Verify the result + assertFalse(result, "Removing non-existent tool should return false") + assertFalse(toolListChangedNotificationReceived, "No notification should be sent when tool doesn't exist") + } + + @Test + fun `removeTool should throw when tools capability is not supported`() = runTest { + // Create server without tools capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities() + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions + ) + + // Verify that removing a tool throws an exception + val exception = assertThrows { + server.removeTool("test-tool") + } + assertEquals("Server does not support tools capability.", exception.message) + } + + @Test + fun `removeTools should remove multiple tools`() = runTest { + // Create server with tools capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities( + tools = ServerCapabilities.Tools(null) + ) + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions + ) + + // Add tools + server.addTool("test-tool-1", "Test Tool 1") { request -> + CallToolResult(listOf(TextContent("Test result 1"))) + } + server.addTool("test-tool-2", "Test Tool 2") { request -> + CallToolResult(listOf(TextContent("Test result 2"))) + } + + // Setup client + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + ) + + // Connect client and server + launch { client.connect(clientTransport) } + launch { server.connect(serverTransport) } + + // 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") + } + + @Test + fun `removePrompt should remove a prompt`() = runTest { + // Create server with prompts capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities( + prompts = ServerCapabilities.Prompts(listChanged = false) + ) + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions + ) + + // Add a prompt + val testPrompt = Prompt("test-prompt", "Test Prompt", null) + server.addPrompt(testPrompt) { request -> + GetPromptResult( + description = "Test prompt description", + messages = listOf() + ) + } + + // Setup client + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + ) + + // Connect client and server + launch { client.connect(clientTransport) } + launch { server.connect(serverTransport) } + + // Remove the prompt + val result = server.removePrompt(testPrompt.name) + + // Verify the prompt was removed + assertTrue(result, "Prompt should be removed successfully") + } + + @Test + fun `removePrompts should remove multiple prompts and send notification`() = runTest { + // Create server with prompts capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities( + prompts = ServerCapabilities.Prompts(listChanged = false) + ) + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions + ) + + // 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) { request -> + GetPromptResult( + description = "Test prompt description 1", + messages = listOf() + ) + } + server.addPrompt(testPrompt2) { request -> + GetPromptResult( + description = "Test prompt description 2", + messages = listOf() + ) + } + + // Setup client + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + ) + + // Connect client and server + launch { client.connect(clientTransport) } + launch { server.connect(serverTransport) } + + // 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") + } + + @Test + fun `removeResource should remove a resource and send notification`() = runTest { + // Create server with resources capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(null, null) + ) + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions + ) + + // Add a resource + val testResourceUri = "test://resource" + server.addResource( + uri = testResourceUri, + name = "Test Resource", + description = "A test resource", + mimeType = "text/plain" + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = "Test resource content", + uri = testResourceUri, + mimeType = "text/plain", + ), + ) + ) + } + + // Setup client + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + ) + + // Connect client and server + launch { client.connect(clientTransport) } + launch { server.connect(serverTransport) } + + // Remove the resource + val result = server.removeResource(testResourceUri) + + // Verify the resource was removed + assertTrue(result, "Resource should be removed successfully") + } + + @Test + fun `removeResources should remove multiple resources and send notification`() = runTest { + // Create server with resources capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(null, null) + ) + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions + ) + + // 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" + ) { request -> + 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" + ) { request -> + ReadResourceResult( + contents = listOf( + TextResourceContents( + text = "Test resource content 2", + uri = testResourceUri2, + mimeType = "text/plain", + ), + ) + ) + } + + // Setup client + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + ) + + // Connect client and server + launch { client.connect(clientTransport) } + launch { server.connect(serverTransport) } + + // Remove the resources + val result = server.removeResources(listOf(testResourceUri1, testResourceUri2)) + + // Verify the resources were removed + assertEquals(2, result, "Both resources should be removed") + } + + @Test + fun `removePrompt should return false when prompt does not exist`() = runTest { + // Create server with prompts capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities( + prompts = ServerCapabilities.Prompts(listChanged = false) + ) + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions + ) + + // Setup client + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + ) + + // Track notifications + var promptListChangedNotificationReceived = false + client.setNotificationHandler(Method.Defined.NotificationsPromptsListChanged) { + promptListChangedNotificationReceived = true + CompletableDeferred(Unit) + } + + // Connect client and server + launch { client.connect(clientTransport) } + launch { server.connect(serverTransport) } + + // 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") + assertFalse(promptListChangedNotificationReceived, "No notification should be sent when prompt doesn't exist") + } + + @Test + fun `removePrompt should throw when prompts capability is not supported`() = runTest { + // Create server without prompts capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities() + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions + ) + + // Verify that removing a prompt throws an exception + val exception = assertThrows { + server.removePrompt("test-prompt") + } + assertEquals("Server does not support prompts capability.", exception.message) + } + + @Test + fun `removeResource should return false when resource does not exist`() = runTest { + // Create server with resources capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities( + resources = ServerCapabilities.Resources(null, null) + ) + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions + ) + + // Setup client + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + ) + + // Track notifications + var resourceListChangedNotificationReceived = false + client.setNotificationHandler(Method.Defined.NotificationsResourcesListChanged) { + resourceListChangedNotificationReceived = true + CompletableDeferred(Unit) + } + + // Connect client and server + launch { client.connect(clientTransport) } + launch { server.connect(serverTransport) } + + // 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") + } + + @Test + fun `removeResource should throw when resources capability is not supported`() = runTest { + // Create server without resources capability + val serverOptions = ServerOptions( + capabilities = ServerCapabilities() + ) + val server = Server( + Implementation(name = "test server", version = "1.0"), + serverOptions + ) + + // Verify that removing a resource throws an exception + val exception = assertThrows { + server.removeResource("test://resource") + } + assertEquals("Server does not support resources capability.", exception.message) + } +}