Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,23 +77,6 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() {
)
}

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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.modelcontextprotocol.kotlin.sdk.server

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
import io.kotest.matchers.collections.shouldHaveSize
Expand Down Expand Up @@ -57,6 +58,39 @@ class ServerBulkFeaturesTest : AbstractServerFeaturesTest() {
tools.map { it.name } shouldContainExactlyInAnyOrder listOf("existing", "new-a", "new-b")
}

@Test
fun `addTools should throw and register nothing when a tool is already registered`() = runTest {
server.addTool("existing", "Existing") { CallToolResult(emptyList()) }

shouldThrow<IllegalArgumentException> {
server.addTools(
listOf(
RegisteredTool(Tool("new-a", ToolSchema())) { CallToolResult(emptyList()) },
RegisteredTool(Tool("existing", ToolSchema())) { CallToolResult(emptyList()) },
),
)
}

val tools = client.listTools().tools

tools shouldHaveSize 1
tools.map { it.name } shouldContainExactlyInAnyOrder listOf("existing")
}

@Test
fun `addTools should throw when batch contains duplicate names`() = runTest {
shouldThrow<IllegalArgumentException> {
server.addTools(
listOf(
RegisteredTool(Tool("dup", ToolSchema())) { CallToolResult(emptyList()) },
RegisteredTool(Tool("dup", ToolSchema())) { CallToolResult(emptyList()) },
),
)
}

client.listTools().tools.shouldBeEmpty()
}

// ── addPrompts ─────────────────────────────────────────────────────────────

@Test
Expand Down Expand Up @@ -89,6 +123,25 @@ class ServerBulkFeaturesTest : AbstractServerFeaturesTest() {
prompts.map { it.name } shouldContainExactlyInAnyOrder listOf("existing-prompt", "new-p")
}

@Test
fun `addPrompts should throw and register nothing when a prompt is already registered`() = runTest {
server.addPrompt("existing-prompt", "Existing") { GetPromptResult(messages = emptyList()) }

shouldThrow<IllegalArgumentException> {
server.addPrompts(
listOf(
RegisteredPrompt(Prompt("new-p")) { GetPromptResult(messages = emptyList()) },
RegisteredPrompt(Prompt("existing-prompt")) { GetPromptResult(messages = emptyList()) },
),
)
}

val prompts = client.listPrompts().prompts

prompts shouldHaveSize 1
prompts.map { it.name } shouldContainExactlyInAnyOrder listOf("existing-prompt")
}

// ── addResources ───────────────────────────────────────────────────────────

@Test
Expand Down Expand Up @@ -123,6 +176,27 @@ class ServerBulkFeaturesTest : AbstractServerFeaturesTest() {
resources.map { it.uri } shouldContainExactlyInAnyOrder listOf("test://existing-res", "test://new-res")
}

@Test
fun `addResources should throw and register nothing when a resource is already registered`() = runTest {
server.addResource("test://existing-res", "Existing", "Existing resource") {
ReadResourceResult(emptyList())
}

shouldThrow<IllegalArgumentException> {
server.addResources(
listOf(
RegisteredResource(Resource("test://new-res", "New")) { ReadResourceResult(emptyList()) },
RegisteredResource(Resource("test://existing-res", "Dup")) { ReadResourceResult(emptyList()) },
),
)
}

val resources = client.listResources().resources

resources shouldHaveSize 1
resources.map { it.uri } shouldContainExactlyInAnyOrder listOf("test://existing-res")
}

// ── Partial-batch remove ───────────────────────────────────────────────────

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.modelcontextprotocol.kotlin.sdk.server

import io.kotest.matchers.string.shouldContain
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import io.modelcontextprotocol.kotlin.sdk.types.Method
Expand Down Expand Up @@ -110,6 +113,26 @@ class ServerPromptsTest : AbstractServerFeaturesTest() {
assertFalse(promptListChangedNotificationReceived, "No notification should be sent when prompt doesn't exist")
}

@Test
fun `addPrompt should throw when prompt with same name is already registered`() = runTest {
server.addPrompt(Prompt("test-prompt", "Original Prompt", null)) {
GetPromptResult(description = "original", messages = emptyList())
}

val exception = assertThrows<IllegalArgumentException> {
server.addPrompt(Prompt("test-prompt", "Duplicate Prompt", null)) {
GetPromptResult(description = "duplicate", messages = emptyList())
}
}
exception.message.orEmpty() shouldContain "Prompt \"test-prompt\" is already registered"

// The original registration is intact
val prompts = client.listPrompts().prompts
assertEquals(1, prompts.size, "Only the original prompt should be registered")
val result = client.getPrompt(GetPromptRequest(GetPromptRequestParams("test-prompt")))
assertEquals("original", result.description)
}

@Test
fun `removePrompt should throw when prompts capability is not supported`() = runTest {
var promptListChangedNotificationReceived = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.maps.shouldContainKey
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest
import io.modelcontextprotocol.kotlin.sdk.types.McpException
Expand Down Expand Up @@ -162,6 +163,24 @@ class ServerResourceTemplateTest : AbstractServerFeaturesTest() {
removed shouldBe false
}

@Test
fun `addResourceTemplate should throw when template with same uriTemplate is already registered`() {
server.addResourceTemplate("test://items/{id}", "Item") { _, _ ->
ReadResourceResult(emptyList())
}

val exception = assertThrows<IllegalArgumentException> {
server.addResourceTemplate("test://items/{id}", "Duplicate Item") { _, _ ->
ReadResourceResult(emptyList())
}
}
exception.message.orEmpty() shouldContain "ResourceTemplate \"test://items/{id}\" is already registered"

// The original registration is intact
server.resourceTemplates shouldHaveSize 1
server.resourceTemplates[0].name shouldBe "Item"
}

@Test
fun `addResourceTemplate should throw when resources capability is not supported`() {
val noResourcesServer = Server(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package io.modelcontextprotocol.kotlin.sdk.server

import io.kotest.matchers.string.shouldContain
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import io.modelcontextprotocol.kotlin.sdk.types.Method
import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest
import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams
import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult
import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
Expand Down Expand Up @@ -115,6 +118,24 @@ class ServerResourcesTest : AbstractServerFeaturesTest() {
)
}

@Test
fun `addResource should throw when resource with same uri is already registered`() = runTest {
server.addResource("test://resource", "Original", "Original resource") {
ReadResourceResult(listOf(TextResourceContents(text = "original", uri = "test://resource")))
}

val exception = assertThrows<IllegalArgumentException> {
server.addResource("test://resource", "Duplicate", "Duplicate resource") {
ReadResourceResult(listOf(TextResourceContents(text = "duplicate", uri = "test://resource")))
}
}
exception.message.orEmpty() shouldContain "Resource \"test://resource\" is already registered"

// The original registration is intact
val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://resource")))
assertEquals("original", (result.contents.single() as TextResourceContents).text)
}

@Test
fun `removeResource should throw when resources capability is not supported`() = runTest {
// Create server without resources capability
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import kotlinx.coroutines.test.runTest
import org.awaitility.kotlin.await
import org.awaitility.kotlin.untilAsserted
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
Expand Down Expand Up @@ -81,6 +82,27 @@ class ServerToolsNotificationTest : AbstractServerFeaturesTest() {
}
}

@Test
fun `addTool should not send notification when duplicate is rejected`() = runTest {
// Track notifications
val notifications = mutableListOf<ToolListChangedNotification>()
client.setNotificationHandler<ToolListChangedNotification>(
Method.Defined.NotificationsToolsListChanged,
) { notification ->
notifications.add(notification)
CompletableDeferred(Unit)
}

server.addTool("test-tool", "Test Tool") { CallToolResult(emptyList()) }
assertThrows<IllegalArgumentException> {
server.addTool("test-tool", "Duplicate Tool") { CallToolResult(emptyList()) }
}
// Close the server to stop processing further events and flush notifications
server.close()

assertEquals(1, notifications.size, "Only the first registration should send a notification")
}
Comment on lines +96 to +104

@Test
fun `notification should not be send when removed tool does not exists`() = runTest {
// Track notifications
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.modelcontextprotocol.kotlin.sdk.server

import io.kotest.matchers.string.shouldContain
import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest
import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams
import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import io.modelcontextprotocol.kotlin.sdk.types.Method
Expand Down Expand Up @@ -108,6 +111,42 @@ class ServerToolsTest : AbstractServerFeaturesTest() {
assertTrue(result, "Tool with non-conforming name should have been registered and removable")
}

@Test
fun `addTool should throw when tool with same name is already registered`() = runTest {
server.addTool("test-tool", "Original Tool") {
CallToolResult(listOf(TextContent("Original result")))
}

val exception = assertThrows<IllegalArgumentException> {
server.addTool("test-tool", "Duplicate Tool") {
CallToolResult(listOf(TextContent("Duplicate result")))
}
}
exception.message.orEmpty() shouldContain "Tool \"test-tool\" is already registered"

// The original registration is intact
val tools = client.listTools().tools
assertEquals(1, tools.size, "Only the original tool should be registered")
val result = client.callTool(CallToolRequest(CallToolRequestParams("test-tool")))
assertEquals("Original result", (result.content.single() as TextContent).text)
}

@Test
fun `addTool should succeed after removing existing tool`() = runTest {
server.addTool("test-tool", "Original Tool") {
CallToolResult(listOf(TextContent("Original result")))
}

server.removeTool("test-tool")
server.addTool("test-tool", "Replacement Tool") {
CallToolResult(listOf(TextContent("Replacement result")))
}

assertEquals(1, client.listTools().tools.size, "Re-adding after removal should not duplicate the tool")
val result = client.callTool(CallToolRequest(CallToolRequestParams("test-tool")))
assertEquals("Replacement result", (result.content.single() as TextContent).text)
}

@Test
fun `removeTool should throw when tools capability is not supported`() = runTest {
var toolListChangedNotificationReceived = false
Expand Down
Loading
Loading