diff --git a/integration-test/detekt-baseline-test.xml b/integration-test/detekt-baseline-test.xml
index 46849e9..1e93fe0 100644
--- a/integration-test/detekt-baseline-test.xml
+++ b/integration-test/detekt-baseline-test.xml
@@ -32,7 +32,6 @@
LongMethod:KotlinServerForTsClientSse.kt:HttpServerTransport$suspend fun handleRequest
LongMethod:KotlinServerForTsClientSse.kt:KotlinServerForTsClient$fun createMcpServer: Server
LongMethod:KotlinServerForTsClientSse.kt:KotlinServerForTsClient$fun start
- LongMethod:KotlinTestBase.kt:KotlinTestBase$protected fun setupServer
LongMethod:ServerResourcesNotificationSubscribeTest.kt:ServerResourcesNotificationSubscribeTest$@Test fun `should send resource notifications`
LongMethod:TsEdgeCasesTestSse.kt:TsEdgeCasesTestSse$@Test @Timeout(30, unit = TimeUnit.SECONDS) fun testComplexConcurrentRequests: Unit
MatchingDeclarationName:PromptIntegrationTestSse.kt:SchemaPromptIntegrationTestSse : AbstractPromptIntegrationTest
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractServerFeaturesTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractServerFeaturesTest.kt
index 21b46d2..1d28218 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractServerFeaturesTest.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractServerFeaturesTest.kt
@@ -1,9 +1,16 @@
package io.modelcontextprotocol.kotlin.sdk.server
import io.modelcontextprotocol.kotlin.sdk.client.Client
+import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions
import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport
+import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
+import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
+import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
+import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
+import io.modelcontextprotocol.kotlin.sdk.types.TextContent
+import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
@@ -13,8 +20,31 @@ abstract class AbstractServerFeaturesTest {
protected lateinit var server: Server
protected lateinit var client: Client
+ protected fun addTool(name: String, block: suspend ClientConnection.() -> Unit) {
+ server.addTool(name, "Test $name") {
+ block()
+ CallToolResult(listOf(TextContent("Success")))
+ }
+ }
+
+ protected fun addPrompt(name: String, block: suspend ClientConnection.() -> Unit) {
+ server.addPrompt(name, "Test $name") {
+ block()
+ GetPromptResult(messages = emptyList())
+ }
+ }
+
+ protected fun addResource(uri: String, block: suspend ClientConnection.() -> Unit) {
+ server.addResource(uri, uri, "Test resource $uri") {
+ block()
+ ReadResourceResult(contents = listOf(TextResourceContents(text = "content", uri = uri)))
+ }
+ }
+
abstract fun getServerCapabilities(): ServerCapabilities
+ protected open fun getClientCapabilities(): ClientCapabilities = ClientCapabilities()
+
protected open fun getServerInstructionsProvider(): (() -> String)? = null
@BeforeEach
@@ -33,6 +63,9 @@ abstract class AbstractServerFeaturesTest {
client = Client(
clientInfo = Implementation(name = "test client", version = "1.0"),
+ options = ClientOptions(
+ capabilities = getClientCapabilities(),
+ ),
)
runBlocking {
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionLoggingTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionLoggingTest.kt
new file mode 100644
index 0000000..81a3b41
--- /dev/null
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionLoggingTest.kt
@@ -0,0 +1,172 @@
+package io.modelcontextprotocol.kotlin.sdk.server
+
+import io.kotest.matchers.collections.shouldBeEmpty
+import io.kotest.matchers.collections.shouldHaveSize
+import io.kotest.matchers.shouldBe
+import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest
+import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams
+import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel
+import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification
+import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams
+import io.modelcontextprotocol.kotlin.sdk.types.Method
+import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.EnumSource
+
+class ClientConnectionLoggingTest : AbstractServerFeaturesTest() {
+
+ override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities(
+ tools = ServerCapabilities.Tools(listChanged = true),
+ logging = JsonObject(emptyMap()),
+ )
+
+ @Test
+ fun `notification should send logging message to client`(): Unit = runBlocking {
+ val notificationReceived = CompletableDeferred()
+ client.setNotificationHandler(Method.Defined.NotificationsMessage) {
+ notificationReceived.complete(it)
+ CompletableDeferred(Unit)
+ }
+
+ val expectedLevel = LoggingLevel.Info
+ val expectedData = JsonPrimitive("test-data-sample")
+
+ addTool("test-notification") {
+ notification(
+ LoggingMessageNotification(
+ LoggingMessageNotificationParams(
+ level = expectedLevel,
+ data = expectedData,
+ ),
+ ),
+ )
+ }
+
+ client.callTool(CallToolRequest(CallToolRequestParams("test-notification")))
+ val received = notificationReceived.await()
+ received.params.level shouldBe expectedLevel
+ received.params.data shouldBe expectedData
+ }
+
+ @ParameterizedTest
+ @EnumSource(LoggingLevel::class)
+ fun `notification should filter logging messages below level`(minLevel: LoggingLevel): Unit = runBlocking {
+ val receivedMessages = mutableListOf()
+ client.setNotificationHandler(Method.Defined.NotificationsMessage) {
+ receivedMessages.add(it)
+ CompletableDeferred(Unit)
+ }
+
+ client.setLoggingLevel(minLevel)
+
+ addTool("test-notification-filtered") {
+ LoggingLevel.entries.forEach { level ->
+ notification(
+ LoggingMessageNotification(
+ LoggingMessageNotificationParams(
+ level = level,
+ data = JsonPrimitive(level.name),
+ ),
+ ),
+ )
+ }
+ }
+
+ client.callTool(CallToolRequest(CallToolRequestParams("test-notification-filtered")))
+
+ val expectedLevels = LoggingLevel.entries.filter { it >= minLevel }
+ receivedMessages shouldHaveSize expectedLevels.size
+ receivedMessages.map { it.params.level } shouldBe expectedLevels
+ }
+
+ @ParameterizedTest
+ @EnumSource(LoggingLevel::class)
+ fun `sendLoggingMessage should send message at level`(expectedLevel: LoggingLevel): Unit = runBlocking {
+ val notificationReceived = CompletableDeferred()
+ client.setNotificationHandler(Method.Defined.NotificationsMessage) {
+ notificationReceived.complete(it)
+ CompletableDeferred(Unit)
+ }
+
+ val expectedData = JsonObject(mapOf("key" to JsonPrimitive("value")))
+
+ addTool("test-logging") {
+ sendLoggingMessage(
+ LoggingMessageNotification(
+ LoggingMessageNotificationParams(
+ level = expectedLevel,
+ data = expectedData,
+ ),
+ ),
+ )
+ }
+
+ client.callTool(CallToolRequest(CallToolRequestParams("test-logging")))
+ val received = notificationReceived.await()
+ received.params.level shouldBe expectedLevel
+ received.params.data shouldBe expectedData
+ }
+
+ @ParameterizedTest
+ @EnumSource(LoggingLevel::class)
+ fun `sendLoggingMessage should filter messages below level`(minLevel: LoggingLevel): Unit = runBlocking {
+ val receivedMessages = mutableListOf()
+ client.setNotificationHandler(Method.Defined.NotificationsMessage) {
+ receivedMessages.add(it)
+ CompletableDeferred(Unit)
+ }
+
+ client.setLoggingLevel(minLevel)
+
+ addTool("test-logging-level") {
+ LoggingLevel.entries.forEach { level ->
+ sendLoggingMessage(
+ LoggingMessageNotification(
+ LoggingMessageNotificationParams(
+ level = level,
+ data = JsonPrimitive(level.name),
+ ),
+ ),
+ )
+ }
+ }
+
+ client.callTool(CallToolRequest(CallToolRequestParams("test-logging-level")))
+
+ val expectedLevels = LoggingLevel.entries.filter { it >= minLevel }
+ receivedMessages shouldHaveSize expectedLevels.size
+ receivedMessages.map { it.params.level } shouldBe expectedLevels
+ }
+
+ @Test
+ fun `sendLoggingMessage should send no messages when level is set to highest`(): Unit = runBlocking {
+ val receivedMessages = mutableListOf()
+ client.setNotificationHandler(Method.Defined.NotificationsMessage) {
+ receivedMessages.add(it)
+ CompletableDeferred(Unit)
+ }
+
+ client.setLoggingLevel(LoggingLevel.Emergency)
+
+ addTool("test-logging-highest") {
+ LoggingLevel.entries.dropLast(1).forEach { level ->
+ sendLoggingMessage(
+ LoggingMessageNotification(
+ LoggingMessageNotificationParams(
+ level = level,
+ data = JsonPrimitive(level.name),
+ ),
+ ),
+ )
+ }
+ }
+
+ client.callTool(CallToolRequest(CallToolRequestParams("test-logging-highest")))
+ receivedMessages.shouldBeEmpty()
+ }
+}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt
new file mode 100644
index 0000000..71f8129
--- /dev/null
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt
@@ -0,0 +1,163 @@
+package io.modelcontextprotocol.kotlin.sdk.server
+
+import io.kotest.assertions.assertSoftly
+import io.kotest.assertions.withClue
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.string.shouldNotBeBlank
+import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest
+import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams
+import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
+import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest
+import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams
+import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest
+import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult
+import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel
+import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification
+import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams
+import io.modelcontextprotocol.kotlin.sdk.types.Method
+import io.modelcontextprotocol.kotlin.sdk.types.Notification
+import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification
+import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest
+import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams
+import io.modelcontextprotocol.kotlin.sdk.types.Request
+import io.modelcontextprotocol.kotlin.sdk.types.RequestResult
+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.Root
+import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
+import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.test.runTest
+import kotlinx.serialization.json.JsonPrimitive
+import org.junit.jupiter.api.Test
+
+class ClientConnectionTest : AbstractServerFeaturesTest() {
+
+ private inline fun onClientNotification(method: Method): CompletableDeferred {
+ val deferred = CompletableDeferred()
+ client.setNotificationHandler(method) {
+ deferred.complete(it)
+ CompletableDeferred(Unit)
+ }
+ return deferred
+ }
+
+ private inline fun onClientRequest(
+ method: Method,
+ crossinline response: (Req) -> Res,
+ ) {
+ client.setRequestHandler(method) { req, _ -> response(req) }
+ }
+
+ override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities(
+ tools = ServerCapabilities.Tools(listChanged = true),
+ prompts = ServerCapabilities.Prompts(listChanged = true),
+ resources = ServerCapabilities.Resources(listChanged = true),
+ logging = ServerCapabilities.Logging,
+ )
+
+ override fun getClientCapabilities(): ClientCapabilities = ClientCapabilities(
+ roots = ClientCapabilities.Roots(listChanged = true),
+ )
+
+ private val sampleRoots = listOf(Root("file:///project", "Project Root"))
+
+ /** Holds all deferreds that capture what a server-side handler emits during [callAllMethods]. */
+ private inner class Captures {
+ val pingComplete = CompletableDeferred()
+ val rootsResult = CompletableDeferred()
+ val sessionId = CompletableDeferred()
+ val toolListChanged =
+ onClientNotification(Method.Defined.NotificationsToolsListChanged)
+ val resourceListChanged =
+ onClientNotification(Method.Defined.NotificationsResourcesListChanged)
+ val promptListChanged =
+ onClientNotification(Method.Defined.NotificationsPromptsListChanged)
+ val loggingMessage = onClientNotification(Method.Defined.NotificationsMessage)
+ val resourceUpdated =
+ onClientNotification(Method.Defined.NotificationsResourcesUpdated)
+
+ init {
+ onClientRequest(Method.Defined.RootsList) {
+ ListRootsResult(sampleRoots)
+ }
+ }
+
+ suspend fun assertAll() {
+ val roots = rootsResult.await()
+ val log = loggingMessage.await()
+ val resourceUri = resourceUpdated.await().params.uri
+ val capturedSessionId = sessionId.await()
+ assertSoftly {
+ withClue("ping() was not called inside the handler") {
+ pingComplete.isCompleted shouldBe true
+ }
+ roots.roots shouldBe sampleRoots
+ withClue("sendToolListChanged() did not deliver a notification to the client") {
+ toolListChanged.isCompleted shouldBe true
+ }
+ withClue("sendResourceListChanged() did not deliver a notification to the client") {
+ resourceListChanged.isCompleted shouldBe true
+ }
+ withClue("sendPromptListChanged() did not deliver a notification to the client") {
+ promptListChanged.isCompleted shouldBe true
+ }
+ withClue("sendLoggingMessage() delivered unexpected payload") {
+ log.params.data shouldBe JsonPrimitive("log")
+ }
+ withClue("sendResourceUpdated() delivered wrong URI") {
+ resourceUri shouldBe "test://res"
+ }
+ capturedSessionId.shouldNotBeBlank()
+ }
+ }
+ }
+
+ /** Exercises every [ClientConnection] method, recording results into [cap]. */
+ private suspend fun ClientConnection.callAllMethods(cap: Captures) {
+ ping()
+ cap.pingComplete.complete(Unit)
+ cap.rootsResult.complete(listRoots())
+ sendToolListChanged()
+ sendResourceListChanged()
+ sendPromptListChanged()
+ sendLoggingMessage(
+ LoggingMessageNotification(
+ LoggingMessageNotificationParams(level = LoggingLevel.Info, data = JsonPrimitive("log")),
+ ),
+ )
+ sendResourceUpdated(ResourceUpdatedNotification(ResourceUpdatedNotificationParams("test://res")))
+ cap.sessionId.complete(sessionId)
+ }
+
+ @Test
+ fun `all ClientConnection methods are callable from tool handler`() = runTest {
+ val cap = Captures()
+ addTool("test") { callAllMethods(cap) }
+
+ client.callTool(CallToolRequest(CallToolRequestParams("test")))
+
+ cap.assertAll()
+ }
+
+ @Test
+ fun `all ClientConnection methods are callable from prompt handler`() = runTest {
+ val cap = Captures()
+ addPrompt("test") { callAllMethods(cap) }
+
+ client.getPrompt(GetPromptRequest(GetPromptRequestParams("test")))
+
+ cap.assertAll()
+ }
+
+ @Test
+ fun `all ClientConnection methods are callable from resource handler`() = runTest {
+ val cap = Captures()
+ addResource("test://resource") { callAllMethods(cap) }
+
+ client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://resource")))
+
+ cap.assertAll()
+ }
+}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerBulkFeaturesTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerBulkFeaturesTest.kt
new file mode 100644
index 0000000..1eb828f
--- /dev/null
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerBulkFeaturesTest.kt
@@ -0,0 +1,183 @@
+package io.modelcontextprotocol.kotlin.sdk.server
+
+import io.kotest.matchers.collections.shouldBeEmpty
+import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
+import io.kotest.matchers.collections.shouldHaveSize
+import io.kotest.matchers.shouldBe
+import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
+import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult
+import io.modelcontextprotocol.kotlin.sdk.types.Prompt
+import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult
+import io.modelcontextprotocol.kotlin.sdk.types.Resource
+import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
+import io.modelcontextprotocol.kotlin.sdk.types.Tool
+import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.Test
+
+class ServerBulkFeaturesTest : AbstractServerFeaturesTest() {
+
+ override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities(
+ tools = ServerCapabilities.Tools(listChanged = null),
+ prompts = ServerCapabilities.Prompts(listChanged = null),
+ resources = ServerCapabilities.Resources(listChanged = null, subscribe = null),
+ )
+
+ // ── addTools ───────────────────────────────────────────────────────────────
+
+ @Test
+ fun `addTools should register all provided tools`() = runTest {
+ server.addTools(
+ listOf(
+ RegisteredTool(Tool("bulk-a", ToolSchema(), "Tool A")) { CallToolResult(emptyList()) },
+ RegisteredTool(Tool("bulk-b", ToolSchema(), "Tool B")) { CallToolResult(emptyList()) },
+ RegisteredTool(Tool("bulk-c", ToolSchema(), "Tool C")) { CallToolResult(emptyList()) },
+ ),
+ )
+
+ val tools = client.listTools().tools
+
+ tools shouldHaveSize 3
+ tools.map { it.name } shouldContainExactlyInAnyOrder listOf("bulk-a", "bulk-b", "bulk-c")
+ }
+
+ @Test
+ fun `addTools should append to existing tools`() = runTest {
+ server.addTool("existing", "Existing") { CallToolResult(emptyList()) }
+ server.addTools(
+ listOf(
+ RegisteredTool(Tool("new-a", ToolSchema())) { CallToolResult(emptyList()) },
+ RegisteredTool(Tool("new-b", ToolSchema())) { CallToolResult(emptyList()) },
+ ),
+ )
+
+ val tools = client.listTools().tools
+
+ tools shouldHaveSize 3
+ tools.map { it.name } shouldContainExactlyInAnyOrder listOf("existing", "new-a", "new-b")
+ }
+
+ // ── addPrompts ─────────────────────────────────────────────────────────────
+
+ @Test
+ fun `addPrompts should register all provided prompts`() = runTest {
+ server.addPrompts(
+ listOf(
+ RegisteredPrompt(Prompt("bulk-p1", "Prompt 1")) { GetPromptResult(messages = emptyList()) },
+ RegisteredPrompt(Prompt("bulk-p2", "Prompt 2")) { GetPromptResult(messages = emptyList()) },
+ ),
+ )
+
+ val prompts = client.listPrompts().prompts
+
+ prompts shouldHaveSize 2
+ prompts.map { it.name } shouldContainExactlyInAnyOrder listOf("bulk-p1", "bulk-p2")
+ }
+
+ @Test
+ fun `addPrompts should append to existing prompts`() = runTest {
+ server.addPrompt("existing-prompt", "Existing") { GetPromptResult(messages = emptyList()) }
+ server.addPrompts(
+ listOf(
+ RegisteredPrompt(Prompt("new-p")) { GetPromptResult(messages = emptyList()) },
+ ),
+ )
+
+ val prompts = client.listPrompts().prompts
+
+ prompts shouldHaveSize 2
+ prompts.map { it.name } shouldContainExactlyInAnyOrder listOf("existing-prompt", "new-p")
+ }
+
+ // ── addResources ───────────────────────────────────────────────────────────
+
+ @Test
+ fun `addResources should register all provided resources`() = runTest {
+ server.addResources(
+ listOf(
+ RegisteredResource(Resource("test://bulk-r1", "Resource 1")) { ReadResourceResult(emptyList()) },
+ RegisteredResource(Resource("test://bulk-r2", "Resource 2")) { ReadResourceResult(emptyList()) },
+ ),
+ )
+
+ val resources = client.listResources().resources
+
+ resources shouldHaveSize 2
+ resources.map { it.uri } shouldContainExactlyInAnyOrder listOf("test://bulk-r1", "test://bulk-r2")
+ }
+
+ @Test
+ fun `addResources should append to existing resources`() = runTest {
+ server.addResource("test://existing-res", "Existing", "Existing resource") {
+ ReadResourceResult(emptyList())
+ }
+ server.addResources(
+ listOf(
+ RegisteredResource(Resource("test://new-res", "New")) { ReadResourceResult(emptyList()) },
+ ),
+ )
+
+ val resources = client.listResources().resources
+
+ resources shouldHaveSize 2
+ resources.map { it.uri } shouldContainExactlyInAnyOrder listOf("test://existing-res", "test://new-res")
+ }
+
+ // ── Partial-batch remove ───────────────────────────────────────────────────
+
+ @Test
+ fun `removeTools should return count of actually removed tools`() = runTest {
+ server.addTool("present", "Present") { CallToolResult(emptyList()) }
+ client.listTools().tools shouldHaveSize 1
+
+ val count = server.removeTools(listOf("present", "absent-1", "absent-2"))
+
+ count shouldBe 1
+ client.listTools().tools.shouldBeEmpty()
+ }
+
+ @Test
+ fun `removeTools on empty list should return zero`() = runTest {
+ val count = server.removeTools(emptyList())
+
+ count shouldBe 0
+ }
+
+ @Test
+ fun `removePrompts should return count of actually removed prompts`() = runTest {
+ server.addPrompt("present-p", "Present") { GetPromptResult(messages = emptyList()) }
+ client.listPrompts().prompts shouldHaveSize 1
+
+ val count = server.removePrompts(listOf("present-p", "absent-p"))
+
+ count shouldBe 1
+ client.listPrompts().prompts.shouldBeEmpty()
+ }
+
+ @Test
+ fun `removePrompts on empty list should return zero`() = runTest {
+ val count = server.removePrompts(emptyList())
+
+ count shouldBe 0
+ }
+
+ @Test
+ fun `removeResources should return count of actually removed resources`() = runTest {
+ server.addResource("test://present-r", "Present", "Present resource") {
+ ReadResourceResult(emptyList())
+ }
+ client.listResources().resources shouldHaveSize 1
+
+ val count = server.removeResources(listOf("test://present-r", "test://absent-r"))
+
+ count shouldBe 1
+ client.listResources().resources.shouldBeEmpty()
+ }
+
+ @Test
+ fun `removeResources on empty list should return zero`() = runTest {
+ val count = server.removeResources(emptyList())
+
+ count shouldBe 0
+ }
+}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesInvocationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesInvocationTest.kt
new file mode 100644
index 0000000..52db367
--- /dev/null
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesInvocationTest.kt
@@ -0,0 +1,222 @@
+package io.modelcontextprotocol.kotlin.sdk.server
+
+import io.kotest.assertions.assertSoftly
+import io.kotest.matchers.collections.shouldHaveSize
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.shouldNotBe
+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.GetPromptRequest
+import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams
+import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult
+import io.modelcontextprotocol.kotlin.sdk.types.McpException
+import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage
+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.Role
+import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
+import io.modelcontextprotocol.kotlin.sdk.types.TextContent
+import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.test.runTest
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.jsonPrimitive
+import org.junit.jupiter.api.Test
+import kotlin.test.assertFailsWith
+
+class ServerFeaturesInvocationTest : AbstractServerFeaturesTest() {
+
+ override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities(
+ tools = ServerCapabilities.Tools(listChanged = null),
+ prompts = ServerCapabilities.Prompts(listChanged = null),
+ resources = ServerCapabilities.Resources(listChanged = null, subscribe = null),
+ )
+
+ // ── Tool invocation ────────────────────────────────────────────────────────
+
+ @Test
+ fun `callTool should return tool result content`() = runTest {
+ server.addTool("echo", "Echo tool") {
+ CallToolResult(listOf(TextContent("echo response")))
+ }
+
+ val result = client.callTool(CallToolRequest(CallToolRequestParams("echo")))
+
+ assertSoftly(result) {
+ isError shouldNotBe true
+ (content.single() as TextContent).text shouldBe "echo response"
+ }
+ }
+
+ @Test
+ fun `callTool should pass arguments to handler`() = runTest {
+ server.addTool("greet", "Greeting tool") { request ->
+ val name = request.params.arguments?.get("name")?.jsonPrimitive?.content ?: "stranger"
+ CallToolResult(listOf(TextContent("Hello, $name!")))
+ }
+
+ val result = client.callTool(
+ CallToolRequest(
+ CallToolRequestParams(
+ name = "greet",
+ arguments = JsonObject(mapOf("name" to JsonPrimitive("World"))),
+ ),
+ ),
+ )
+
+ (result.content.single() as TextContent).text shouldBe "Hello, World!"
+ }
+
+ @Test
+ fun `callTool should return error result when tool not found`() = runTest {
+ val result = client.callTool(CallToolRequest(CallToolRequestParams("nonexistent")))
+
+ assertSoftly(result) {
+ isError shouldBe true
+ (content.single() as TextContent).text shouldBe "Tool nonexistent not found"
+ }
+ }
+
+ @Test
+ fun `callTool should return error result when handler throws`() = runTest {
+ server.addTool("failing", "Failing tool") {
+ throw IllegalStateException("handler failure")
+ }
+
+ val result = client.callTool(CallToolRequest(CallToolRequestParams("failing")))
+
+ assertSoftly(result) {
+ isError shouldBe true
+ (content.single() as TextContent).text shouldBe "Error executing tool failing: handler failure"
+ }
+ }
+
+ @Test
+ fun `callTool should pass empty arguments map to handler`() = runTest {
+ server.addTool("greet", "Greeting tool") { request ->
+ val name = request.params.arguments?.get("name")?.jsonPrimitive?.content ?: "stranger"
+ CallToolResult(listOf(TextContent("Hello, $name!")))
+ }
+
+ val result = client.callTool(
+ CallToolRequest(
+ CallToolRequestParams(
+ name = "greet",
+ arguments = JsonObject(emptyMap()),
+ ),
+ ),
+ )
+
+ (result.content.single() as TextContent).text shouldBe "Hello, stranger!"
+ }
+
+ // ── Prompt invocation ──────────────────────────────────────────────────────
+
+ @Test
+ fun `getPrompt should return prompt description and messages`() = runTest {
+ server.addPrompt("my-prompt", "My prompt") {
+ GetPromptResult(
+ description = "Prompt result description",
+ messages = listOf(
+ PromptMessage(role = Role.User, content = TextContent("User message")),
+ ),
+ )
+ }
+
+ val result = client.getPrompt(GetPromptRequest(GetPromptRequestParams("my-prompt")))
+
+ assertSoftly(result) {
+ description shouldBe "Prompt result description"
+ messages shouldHaveSize 1
+ (messages.single().content as TextContent).text shouldBe "User message"
+ }
+ }
+
+ @Test
+ fun `getPrompt should pass arguments to handler`() = runTest {
+ server.addPrompt("templated", "Templated prompt") { request ->
+ val topic = request.params.arguments?.get("topic") ?: "unknown"
+ GetPromptResult(
+ messages = listOf(
+ PromptMessage(role = Role.User, content = TextContent("Tell me about $topic")),
+ ),
+ )
+ }
+
+ val result = client.getPrompt(
+ GetPromptRequest(
+ GetPromptRequestParams(
+ name = "templated",
+ arguments = mapOf("topic" to "Kotlin coroutines"),
+ ),
+ ),
+ )
+
+ (result.messages.single().content as TextContent).text shouldBe "Tell me about Kotlin coroutines"
+ }
+
+ @Test
+ fun `getPrompt should use default when arguments are absent`() = runTest {
+ server.addPrompt("templated", "Templated prompt") { request ->
+ val topic = request.params.arguments?.get("topic") ?: "unknown"
+ GetPromptResult(
+ messages = listOf(
+ PromptMessage(role = Role.User, content = TextContent("Tell me about $topic")),
+ ),
+ )
+ }
+
+ val result = client.getPrompt(GetPromptRequest(GetPromptRequestParams("templated")))
+
+ (result.messages.single().content as TextContent).text shouldBe "Tell me about unknown"
+ }
+
+ @Test
+ fun `getPrompt should throw when prompt not found`() = runTest {
+ assertFailsWith {
+ client.getPrompt(GetPromptRequest(GetPromptRequestParams("nonexistent")))
+ }
+ }
+
+ // ── Resource invocation ────────────────────────────────────────────────────
+
+ @Test
+ fun `readResource should return resource content`() = runTest {
+ val uri = "test://my-resource"
+ server.addResource(uri, "My Resource", "Test resource") {
+ ReadResourceResult(
+ contents = listOf(TextResourceContents(text = "resource content", uri = uri)),
+ )
+ }
+
+ val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri)))
+
+ (result.contents.single() as TextResourceContents).text shouldBe "resource content"
+ }
+
+ @Test
+ fun `readResource should pass request URI to handler`() = runTest {
+ val uri = "test://uri-check"
+ val receivedUri = CompletableDeferred()
+ server.addResource(uri, "URI Check", "Test") { request ->
+ receivedUri.complete(request.params.uri)
+ ReadResourceResult(
+ contents = listOf(TextResourceContents(text = "ok", uri = uri)),
+ )
+ }
+
+ client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri)))
+
+ receivedUri.await() shouldBe uri
+ }
+
+ @Test
+ fun `readResource should throw when resource not found`() = runTest {
+ assertFailsWith {
+ client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://nonexistent")))
+ }
+ }
+}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesListTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesListTest.kt
new file mode 100644
index 0000000..e6aca7f
--- /dev/null
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesListTest.kt
@@ -0,0 +1,221 @@
+package io.modelcontextprotocol.kotlin.sdk.server
+
+import io.kotest.assertions.assertSoftly
+import io.kotest.matchers.collections.shouldBeEmpty
+import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
+import io.kotest.matchers.collections.shouldHaveSize
+import io.kotest.matchers.nulls.shouldNotBeNull
+import io.kotest.matchers.shouldBe
+import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
+import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult
+import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest
+import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument
+import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult
+import io.modelcontextprotocol.kotlin.sdk.types.Resource
+import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.Test
+
+class ServerFeaturesListTest : AbstractServerFeaturesTest() {
+
+ override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities(
+ tools = ServerCapabilities.Tools(listChanged = null),
+ prompts = ServerCapabilities.Prompts(listChanged = null),
+ resources = ServerCapabilities.Resources(listChanged = null, subscribe = null),
+ )
+
+ // ── listTools ──────────────────────────────────────────────────────────────
+
+ @Test
+ fun `listTools should return empty list when no tools registered`() = runTest {
+ client.listTools().tools.shouldBeEmpty()
+ }
+
+ @Test
+ fun `listTools should return all registered tools`() = runTest {
+ server.addTool("alpha", "First tool") { CallToolResult(emptyList()) }
+ server.addTool("beta", "Second tool") { CallToolResult(emptyList()) }
+
+ val tools = client.listTools().tools
+
+ tools shouldHaveSize 2
+ tools.map { it.name } shouldContainExactlyInAnyOrder listOf("alpha", "beta")
+ }
+
+ @Test
+ fun `listTools should expose tool name and description`() = runTest {
+ server.addTool("my-tool", "Detailed description") { CallToolResult(emptyList()) }
+
+ val tool = client.listTools().tools.single()
+
+ assertSoftly(tool) {
+ name shouldBe "my-tool"
+ description shouldBe "Detailed description"
+ }
+ }
+
+ @Test
+ fun `listTools should reflect tool removal`() = runTest {
+ server.addTool("keep", "Kept") { CallToolResult(emptyList()) }
+ server.addTool("drop", "Dropped") { CallToolResult(emptyList()) }
+ client.listTools().tools shouldHaveSize 2
+
+ server.removeTool("drop") shouldBe true
+
+ val tools = client.listTools().tools
+
+ tools shouldHaveSize 1
+ tools.single().name shouldBe "keep"
+ }
+
+ @Test
+ fun `removeTool should return false when tool not found`() = runTest {
+ server.removeTool("ghost") shouldBe false
+ }
+
+ // ── listPrompts ────────────────────────────────────────────────────────────
+
+ @Test
+ fun `listPrompts should return empty list when no prompts registered`() = runTest {
+ client.listPrompts().prompts.shouldBeEmpty()
+ }
+
+ @Test
+ fun `listPrompts should return all registered prompts`() = runTest {
+ server.addPrompt("prompt-a", "First prompt") { GetPromptResult(messages = emptyList()) }
+ server.addPrompt("prompt-b", "Second prompt") { GetPromptResult(messages = emptyList()) }
+
+ val prompts = client.listPrompts().prompts
+
+ prompts shouldHaveSize 2
+ prompts.map { it.name } shouldContainExactlyInAnyOrder listOf("prompt-a", "prompt-b")
+ }
+
+ @Test
+ fun `listPrompts should expose prompt description`() = runTest {
+ server.addPrompt("my-prompt", "Helpful context prompt") { GetPromptResult(messages = emptyList()) }
+
+ val prompt = client.listPrompts().prompts.single()
+
+ assertSoftly(prompt) {
+ name shouldBe "my-prompt"
+ description shouldBe "Helpful context prompt"
+ }
+ }
+
+ @Test
+ fun `listPrompts should include prompt arguments`() = runTest {
+ val args = listOf(
+ PromptArgument(name = "topic", description = "The topic to discuss", required = true),
+ PromptArgument(name = "tone", description = "Tone of voice", required = false),
+ )
+ server.addPrompt("templated", "A templated prompt", args) { GetPromptResult(messages = emptyList()) }
+
+ val prompt = client.listPrompts().prompts.single()
+
+ val promptArgs = prompt.arguments.shouldNotBeNull()
+ promptArgs shouldHaveSize 2
+ promptArgs.map { it.name } shouldContainExactlyInAnyOrder listOf("topic", "tone")
+ promptArgs.first { it.name == "topic" }.required shouldBe true
+ promptArgs.first { it.name == "tone" }.required shouldBe false
+ }
+
+ @Test
+ fun `listPrompts should expose null description`() = runTest {
+ server.addPrompt("no-desc", null) { GetPromptResult(messages = emptyList()) }
+
+ client.listPrompts().prompts.single().description shouldBe null
+ }
+
+ @Test
+ fun `listPrompts should reflect prompt removal`() = runTest {
+ server.addPrompt("keep", "Kept") { GetPromptResult(messages = emptyList()) }
+ server.addPrompt("drop", "Dropped") { GetPromptResult(messages = emptyList()) }
+ client.listPrompts().prompts shouldHaveSize 2
+
+ server.removePrompt("drop") shouldBe true
+
+ val prompts = client.listPrompts().prompts
+
+ prompts shouldHaveSize 1
+ prompts.single().name shouldBe "keep"
+ }
+
+ @Test
+ fun `removePrompt should return false when prompt not found`() = runTest {
+ server.removePrompt("ghost") shouldBe false
+ }
+
+ // ── listResources ──────────────────────────────────────────────────────────
+
+ @Test
+ fun `listResources should return empty list when no resources registered`() = runTest {
+ client.listResources().resources.shouldBeEmpty()
+ }
+
+ @Test
+ fun `listResources should return all registered resources`() = runTest {
+ server.addResource("test://res-1", "Resource 1", "First") { ReadResourceResult(emptyList()) }
+ server.addResource("test://res-2", "Resource 2", "Second") { ReadResourceResult(emptyList()) }
+
+ val resources = client.listResources().resources
+
+ resources shouldHaveSize 2
+ resources.map { it.uri } shouldContainExactlyInAnyOrder listOf("test://res-1", "test://res-2")
+ }
+
+ @Test
+ fun `listResources should expose resource metadata`() = runTest {
+ server.addResource("test://my-res", "My Resource", "A detailed resource", "application/json") {
+ ReadResourceResult(emptyList())
+ }
+
+ val resource = client.listResources().resources.single()
+
+ assertSoftly(resource) {
+ uri shouldBe "test://my-res"
+ name shouldBe "My Resource"
+ description shouldBe "A detailed resource"
+ mimeType shouldBe "application/json"
+ }
+ }
+
+ @Test
+ fun `listResources should expose resource without mimeType`() = runTest {
+ server.addResources(
+ listOf(
+ RegisteredResource(Resource("test://no-mime", "No Mime")) {
+ ReadResourceResult(emptyList())
+ },
+ ),
+ )
+
+ client.listResources().resources.single().mimeType shouldBe null
+ }
+
+ @Test
+ fun `listResources should reflect resource removal`() = runTest {
+ server.addResource("test://keep", "Keep", "Kept") { ReadResourceResult(emptyList()) }
+ server.addResource("test://drop", "Drop", "Dropped") { ReadResourceResult(emptyList()) }
+ client.listResources().resources shouldHaveSize 2
+
+ server.removeResource("test://drop") shouldBe true
+
+ val resources = client.listResources().resources
+
+ resources shouldHaveSize 1
+ resources.single().uri shouldBe "test://keep"
+ }
+
+ @Test
+ fun `removeResource should return false when resource not found`() = runTest {
+ server.removeResource("test://ghost") shouldBe false
+ }
+
+ // ── listResourceTemplates ──────────────────────────────────────────────────
+
+ @Test
+ fun `listResourceTemplates should return empty list`() = runTest {
+ client.listResourceTemplates(ListResourceTemplatesRequest()).resourceTemplates.shouldBeEmpty()
+ }
+}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerHandlerNotificationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerHandlerNotificationTest.kt
new file mode 100644
index 0000000..655d1f1
--- /dev/null
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerHandlerNotificationTest.kt
@@ -0,0 +1,124 @@
+package io.modelcontextprotocol.kotlin.sdk.server
+
+import io.kotest.matchers.shouldBe
+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.GetPromptRequest
+import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams
+import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult
+import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel
+import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification
+import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotificationParams
+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.ServerCapabilities
+import io.modelcontextprotocol.kotlin.sdk.types.TextContent
+import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.test.runTest
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import org.junit.jupiter.api.Test
+
+class ServerHandlerNotificationTest : AbstractServerFeaturesTest() {
+
+ override fun getServerCapabilities(): ServerCapabilities = ServerCapabilities(
+ tools = ServerCapabilities.Tools(true),
+ prompts = ServerCapabilities.Prompts(true),
+ resources = ServerCapabilities.Resources(true, null),
+ logging = JsonObject(emptyMap()),
+ )
+
+ @Test
+ fun `tool handler can send notification to client`() = runTest {
+ val notificationReceived = CompletableDeferred()
+ client.setNotificationHandler(Method.Defined.NotificationsMessage) { notification ->
+ notificationReceived.complete(notification)
+ CompletableDeferred(Unit)
+ }
+
+ val notificationText = "Notification from tool"
+ server.addTool("notify-tool", "A tool that notifies") {
+ sendLoggingMessage(
+ LoggingMessageNotification(
+ LoggingMessageNotificationParams(
+ level = LoggingLevel.Info,
+ data = JsonPrimitive(notificationText),
+ ),
+ ),
+ )
+ CallToolResult(listOf(TextContent("Tool executed")))
+ }
+
+ val result = client.callTool(CallToolRequest(CallToolRequestParams(name = "notify-tool")))
+
+ (result.content[0] as TextContent).text shouldBe "Tool executed"
+ val received = notificationReceived.await()
+ received.params.level shouldBe LoggingLevel.Info
+ received.params.data shouldBe JsonPrimitive(notificationText)
+ }
+
+ @Test
+ fun `prompt handler can send notification to client`() = runTest {
+ val notificationReceived = CompletableDeferred()
+ client.setNotificationHandler(Method.Defined.NotificationsMessage) { notification ->
+ notificationReceived.complete(notification)
+ CompletableDeferred(Unit)
+ }
+
+ val notificationText = "Notification from prompt"
+ server.addPrompt("notify-prompt", "A prompt that notifies") {
+ sendLoggingMessage(
+ LoggingMessageNotification(
+ LoggingMessageNotificationParams(
+ level = LoggingLevel.Info,
+ data = JsonPrimitive(notificationText),
+ ),
+ ),
+ )
+ GetPromptResult(messages = listOf(), description = "Prompt executed")
+ }
+
+ val result = client.getPrompt(GetPromptRequest(GetPromptRequestParams(name = "notify-prompt")))
+
+ result.description shouldBe "Prompt executed"
+ val received = notificationReceived.await()
+ received.params.level shouldBe LoggingLevel.Info
+ received.params.data shouldBe JsonPrimitive(notificationText)
+ }
+
+ @Test
+ fun `resource handler can send notification to client`() = runTest {
+ val notificationReceived = CompletableDeferred()
+ client.setNotificationHandler(Method.Defined.NotificationsMessage) { notification ->
+ notificationReceived.complete(notification)
+ CompletableDeferred(Unit)
+ }
+
+ val resourceUri = "test://notify-resource"
+ val notificationText = "Notification from resource"
+ server.addResource(resourceUri, "Notify Resource", "A resource that notifies") {
+ sendLoggingMessage(
+ LoggingMessageNotification(
+ LoggingMessageNotificationParams(
+ level = LoggingLevel.Info,
+ data = JsonPrimitive(notificationText),
+ ),
+ ),
+ )
+ ReadResourceResult(
+ contents = listOf(TextResourceContents(uri = resourceUri, text = "Resource read")),
+ )
+ }
+
+ val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams(uri = resourceUri)))
+
+ (result.contents[0] as TextResourceContents).text shouldBe "Resource read"
+ val received = notificationReceived.await()
+ received.params.level shouldBe LoggingLevel.Info
+ received.params.data shouldBe JsonPrimitive(notificationText)
+ }
+}
diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api
index f97845b..15bbe5e 100644
--- a/kotlin-sdk-server/api/kotlin-sdk-server.api
+++ b/kotlin-sdk-server/api/kotlin-sdk-server.api
@@ -1,3 +1,33 @@
+public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/ClientConnection {
+ public abstract fun createElicitation (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public abstract fun createElicitation (Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public abstract fun createMessage (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public abstract fun getSessionId ()Ljava/lang/String;
+ public abstract fun listRoots (Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public abstract fun notification (Lio/modelcontextprotocol/kotlin/sdk/types/ServerNotification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public static synthetic fun notification$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ServerNotification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public abstract fun ping (Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public static synthetic fun ping$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public abstract fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public abstract fun sendPromptListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public abstract fun sendResourceListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public abstract fun sendResourceUpdated (Lio/modelcontextprotocol/kotlin/sdk/types/ResourceUpdatedNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public abstract fun sendToolListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/server/ClientConnection$DefaultImpls {
+ public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public static synthetic fun createElicitation$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public static synthetic fun createMessage$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public static synthetic fun listRoots$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public static synthetic fun notification$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/ServerNotification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public static synthetic fun ping$default (Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+}
+
public abstract interface class io/modelcontextprotocol/kotlin/sdk/server/EventStore {
public abstract fun getStreamIdForEventId (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun replayEventsAfter (Ljava/lang/String;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
@@ -15,41 +45,41 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt {
}
public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt : io/modelcontextprotocol/kotlin/sdk/server/Feature {
- public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Lkotlin/jvm/functions/Function2;)V
+ public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Lkotlin/jvm/functions/Function3;)V
public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;
- public final fun component2 ()Lkotlin/jvm/functions/Function2;
- public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;
- public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;
+ public final fun component2 ()Lkotlin/jvm/functions/Function3;
+ public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Lkotlin/jvm/functions/Function3;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;
public fun equals (Ljava/lang/Object;)Z
public fun getKey ()Ljava/lang/String;
- public final fun getMessageProvider ()Lkotlin/jvm/functions/Function2;
+ public final fun getMessageProvider ()Lkotlin/jvm/functions/Function3;
public final fun getPrompt ()Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredResource : io/modelcontextprotocol/kotlin/sdk/server/Feature {
- public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Resource;Lkotlin/jvm/functions/Function2;)V
+ public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Resource;Lkotlin/jvm/functions/Function3;)V
public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Resource;
- public final fun component2 ()Lkotlin/jvm/functions/Function2;
- public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Resource;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;
- public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;Lio/modelcontextprotocol/kotlin/sdk/types/Resource;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;
+ public final fun component2 ()Lkotlin/jvm/functions/Function3;
+ public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Resource;Lkotlin/jvm/functions/Function3;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;Lio/modelcontextprotocol/kotlin/sdk/types/Resource;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;
public fun equals (Ljava/lang/Object;)Z
public fun getKey ()Ljava/lang/String;
- public final fun getReadHandler ()Lkotlin/jvm/functions/Function2;
+ public final fun getReadHandler ()Lkotlin/jvm/functions/Function3;
public final fun getResource ()Lio/modelcontextprotocol/kotlin/sdk/types/Resource;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredTool : io/modelcontextprotocol/kotlin/sdk/server/Feature {
- public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function2;)V
+ public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function3;)V
public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Tool;
- public final fun component2 ()Lkotlin/jvm/functions/Function2;
- public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;
- public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;
+ public final fun component2 ()Lkotlin/jvm/functions/Function3;
+ public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function3;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;
public fun equals (Ljava/lang/Object;)Z
- public final fun getHandler ()Lkotlin/jvm/functions/Function2;
+ public final fun getHandler ()Lkotlin/jvm/functions/Function3;
public fun getKey ()Ljava/lang/String;
public final fun getTool ()Lio/modelcontextprotocol/kotlin/sdk/types/Tool;
public fun hashCode ()I
@@ -61,17 +91,18 @@ public class io/modelcontextprotocol/kotlin/sdk/server/Server {
public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V
public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;Lio/modelcontextprotocol/kotlin/sdk/server/ServerOptions;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public final fun addPrompt (Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Lkotlin/jvm/functions/Function2;)V
- public final fun addPrompt (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;)V
- public static synthetic fun addPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public final fun addPrompt (Lio/modelcontextprotocol/kotlin/sdk/types/Prompt;Lkotlin/jvm/functions/Function3;)V
+ public final fun addPrompt (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function3;)V
+ public static synthetic fun addPrompt$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V
public final fun addPrompts (Ljava/util/List;)V
- public final fun addResource (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V
- public static synthetic fun addResource$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public final fun addResource (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function3;)V
+ public static synthetic fun addResource$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V
public final fun addResources (Ljava/util/List;)V
- public final fun addTool (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function2;)V
- public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function2;)V
- public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+ public final fun addTool (Lio/modelcontextprotocol/kotlin/sdk/types/Tool;Lkotlin/jvm/functions/Function3;)V
+ public final fun addTool (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function3;)V
+ public static synthetic fun addTool$default (Lio/modelcontextprotocol/kotlin/sdk/server/Server;Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lio/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V
public final fun addTools (Ljava/util/List;)V
+ public final fun clientConnection (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/server/ClientConnection;
public final fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun createElicitation (Ljava/lang/String;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
diff --git a/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml
index 67d5e73..d383ec9 100644
--- a/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml
+++ b/kotlin-sdk-server/detekt-baseline-commonMainSourceSet.xml
@@ -5,17 +5,9 @@
MagicNumber:StdioServerTransport.kt:StdioServerTransport$8192
MaxLineLength:SSEServerTransport.kt:SseServerTransport$"SSEServerTransport already started! If using Server class, note that connect() calls start() automatically."
MaxLineLength:SSEServerTransport.kt:SseServerTransport$*
- MaxLineLength:ServerSession.kt:ServerSession$"Client requested unsupported protocol version $requestedVersion, falling back to $LATEST_PROTOCOL_VERSION"
- MaxLineLength:ServerSession.kt:ServerSession$"Creating message with ${params.params.messages.size} messages, maxTokens=${params.params.maxTokens}, temperature=${params.params.temperature}, systemPrompt=${if (params.params.systemPrompt != null) "present" else "absent"}"
ReturnCount:KtorServer.kt:private suspend fun existingStreamableTransport: StreamableHttpServerTransport?
- ThrowsCount:ServerSession.kt:ServerSession$override fun assertCapabilityForMethod
- ThrowsCount:ServerSession.kt:ServerSession$override fun assertNotificationCapability
- ThrowsCount:ServerSession.kt:ServerSession$override fun assertRequestHandlerCapability
TooGenericExceptionCaught:SSEServerTransport.kt:SseServerTransport$e: Exception
- TooGenericExceptionCaught:Server.kt:Server$e: Exception
TooGenericExceptionCaught:StdioServerTransport.kt:StdioServerTransport$e: Throwable
TooManyFunctions:KtorServer.kt:io.modelcontextprotocol.kotlin.sdk.server.KtorServer.kt
- TooManyFunctions:Server.kt:Server
- TooManyFunctions:ServerSession.kt:ServerSession : Protocol
diff --git a/kotlin-sdk-server/detekt-baseline-main.xml b/kotlin-sdk-server/detekt-baseline-main.xml
index c016bed..141c7d0 100644
--- a/kotlin-sdk-server/detekt-baseline-main.xml
+++ b/kotlin-sdk-server/detekt-baseline-main.xml
@@ -4,24 +4,14 @@
InjectDispatcher:FeatureNotificationService.kt:FeatureNotificationService$Default
LongParameterList:KtorServer.kt:private suspend fun RoutingContext.streamableTransport: StreamableHttpServerTransport?
- LongParameterList:Server.kt:Server$public fun addTool
MagicNumber:StdioServerTransport.kt:StdioServerTransport$8192
MaxLineLength:SSEServerTransport.kt:SseServerTransport$"SSEServerTransport already started! If using Server class, note that connect() calls start() automatically."
MaxLineLength:SSEServerTransport.kt:SseServerTransport$*
- MaxLineLength:ServerSession.kt:ServerSession$"Client requested unsupported protocol version $requestedVersion, falling back to $LATEST_PROTOCOL_VERSION"
- MaxLineLength:ServerSession.kt:ServerSession$"Creating message with ${params.params.messages.size} messages, maxTokens=${params.params.maxTokens}, temperature=${params.params.temperature}, systemPrompt=${if (params.params.systemPrompt != null) "present" else "absent"}"
NoNameShadowing:FeatureNotificationService.kt:FeatureNotificationService${ it.remove(job) }
- RedundantSuspendModifier:ServerSession.kt:ServerSession$suspend
ReturnCount:KtorServer.kt:private suspend fun existingStreamableTransport: StreamableHttpServerTransport?
- ThrowsCount:ServerSession.kt:ServerSession$override fun assertCapabilityForMethod
- ThrowsCount:ServerSession.kt:ServerSession$override fun assertNotificationCapability
- ThrowsCount:ServerSession.kt:ServerSession$override fun assertRequestHandlerCapability
TooGenericExceptionCaught:SSEServerTransport.kt:SseServerTransport$e: Exception
- TooGenericExceptionCaught:Server.kt:Server$e: Exception
TooGenericExceptionCaught:StdioServerTransport.kt:StdioServerTransport$e: Throwable
TooManyFunctions:KtorServer.kt:io.modelcontextprotocol.kotlin.sdk.server.KtorServer.kt
- TooManyFunctions:Server.kt:Server
- TooManyFunctions:ServerSession.kt:ServerSession : Protocol
UnsafeCallOnNullableType:StreamableHttpServerTransport.kt:StreamableHttpServerTransport$responseRequestId!!
UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to add prompt '${prompt.name}': Server does not support prompts capability" } "Server does not support prompts capability." }
UseCheckNotNull:Server.kt:Server$check(options.capabilities.prompts != null) { logger.error { "Failed to add prompts: Server does not support prompts capability" } "Server does not support prompts capability." }
@@ -35,17 +25,5 @@
UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to add tools: Server does not support tools capability" } "Server does not support tools capability." }
UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to remove tool '$name': Server does not support tools capability" } "Server does not support tools capability." }
UseCheckNotNull:Server.kt:Server$check(options.capabilities.tools != null) { logger.error { "Failed to remove tools: Server does not support tools capability" } "Server does not support tools capability." }
- UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException( "Server does not support notifying about resources (required for ${method.value})", )
- UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException( "Server does not support notifying of prompt list changes (required for ${method.value})", )
- UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException( "Server does not support notifying of tool list changes (required for ${method.value})", )
- UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Client does not support elicitation (required for ${method.value})")
- UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Client does not support listing roots (required for ${method.value})")
- UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Client does not support sampling (required for ${method.value})")
- UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Server does not support logging (required for $method)")
- UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Server does not support logging (required for ${method.value})")
- UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Server does not support prompts (required for $method)")
- UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Server does not support resources (required for $method)")
- UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Server does not support sampling (required for $method)")
- UseCheckOrError:ServerSession.kt:ServerSession$throw IllegalStateException("Server does not support tools (required for $method)")
diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt
new file mode 100644
index 0000000..998d62c
--- /dev/null
+++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt
@@ -0,0 +1,249 @@
+package io.modelcontextprotocol.kotlin.sdk.server
+
+import io.github.oshai.kotlinlogging.KotlinLogging
+import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions
+import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest
+import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult
+import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest
+import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams
+import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult
+import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult
+import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest
+import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult
+import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel
+import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification
+import io.modelcontextprotocol.kotlin.sdk.types.PingRequest
+import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification
+import io.modelcontextprotocol.kotlin.sdk.types.Request
+import io.modelcontextprotocol.kotlin.sdk.types.RequestId
+import io.modelcontextprotocol.kotlin.sdk.types.RequestResult
+import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification
+import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification
+import io.modelcontextprotocol.kotlin.sdk.types.ServerNotification
+import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification
+
+private val logger = KotlinLogging.logger {}
+
+/**
+ * Represents a connection interface between a server and a client, enabling
+ * communication through notifications, requests, and other operations.
+ * This interface defines various methods to facilitate the interaction.
+ */
+@Suppress("TooManyFunctions")
+public interface ClientConnection {
+
+ /**
+ * A unique identifier identifying the current session.
+ * Has no inherent meaning.
+ */
+ public val sessionId: String
+
+ /**
+ * Sends a server-side notification to the client.
+ *
+ * @param notification The notification to be sent to the client. This typically represents
+ * a specific event or update from the server.
+ * @param relatedRequestId An optional identifier linking this notification to a prior request.
+ * Useful for correlating notifications with originating requests.
+ */
+ public suspend fun notification(notification: ServerNotification, relatedRequestId: RequestId? = null)
+
+ /**
+ * Sends a ping request to the client to check connectivity.
+ *
+ * @param request Optional request parameters for the ping operation.
+ * @param options Optional request options, such as timeout or progress callback settings.
+ * @return The result of the ping request.
+ * @throws IllegalStateException If for some reason the method is not supported or the connection is closed.
+ */
+ public suspend fun ping(request: PingRequest = PingRequest(), options: RequestOptions? = null): EmptyResult
+
+ /**
+ * Creates a message using the server's sampling capability.
+ *
+ * @param request The parameters for creating a message.
+ * @param options Optional request options.
+ * @return The created message result.
+ * @throws IllegalStateException If the server does not support sampling or if the request fails.
+ */
+ public suspend fun createMessage(
+ request: CreateMessageRequest,
+ options: RequestOptions? = null,
+ ): CreateMessageResult
+
+ /**
+ * Lists the available "roots" from the client's perspective (if supported).
+ *
+ * Roots define the boundaries of where servers can operate within the filesystem,
+ * allowing them to understand which directories and files they have access to
+ *
+ * @param request Optional request parameters containing metadata for the list roots operation.
+ * @param options Optional request options, such as timeout or progress callback settings.
+ * @return The list of roots.
+ * @throws IllegalStateException If the server or client does not support roots.
+ */
+ public suspend fun listRoots(
+ request: ListRootsRequest = ListRootsRequest(),
+ options: RequestOptions? = null,
+ ): ListRootsResult
+
+ /**
+ * Sends a message to the client requesting elicitation.
+ * This typically results in a form being displayed to the end user.
+ *
+ * @param message The message for the elicitation to display.
+ * @param requestedSchema The schema requested by the client for the elicitation result.
+ * Influences the form displayed to the user.
+ * @param options Optional request options.
+ * @return The result of the elicitation request.
+ * @throws IllegalStateException If the server or client does not support elicitation.
+ */
+ public suspend fun createElicitation(
+ message: String,
+ requestedSchema: ElicitRequestParams.RequestedSchema,
+ options: RequestOptions? = null,
+ ): ElicitResult
+
+ /**
+ * Sends a message to the client requesting elicitation.
+ * This typically results in a form being displayed to the user.
+ *
+ * @param request The elicitation request parameters.
+ * @param options Optional request options.
+ * @return The result of the elicitation request.
+ * @throws IllegalStateException If the server or client does not support elicitation.
+ */
+ public suspend fun createElicitation(request: ElicitRequest, options: RequestOptions? = null): ElicitResult
+
+ /**
+ * Sends a logging message notification to the client.
+ * Messages are filtered based on the current logging level set by the client.
+ * If no logging level is set, all messages are sent.
+ *
+ * @param notification The logging message notification.
+ */
+ public suspend fun sendLoggingMessage(notification: LoggingMessageNotification)
+
+ /**
+ * Sends a resource-updated notification to the client, indicating that a specific resource has changed.
+ *
+ * @param notification Details of the updated resource.
+ */
+ public suspend fun sendResourceUpdated(notification: ResourceUpdatedNotification)
+
+ /**
+ * Sends a notification to the client indicating that the list of resources has changed.
+ */
+ public suspend fun sendResourceListChanged()
+
+ /**
+ * Sends a notification to the client indicating that the list of tools has changed.
+ */
+ public suspend fun sendToolListChanged()
+
+ /**
+ * Sends a notification to the client indicating that the list of prompts has changed.
+ */
+ public suspend fun sendPromptListChanged()
+}
+
+@Suppress("TooManyFunctions")
+internal class ClientConnectionImpl(private val session: ServerSession) : ClientConnection {
+
+ override val sessionId: String get() = session.sessionId
+
+ private suspend fun request(request: Request, options: RequestOptions? = null): T {
+ logger.trace { "Sending request to client for session $sessionId: $request" }
+ return session.request(request, options)
+ }
+
+ override suspend fun notification(notification: ServerNotification, relatedRequestId: RequestId?) {
+ if (notification is LoggingMessageNotification && !isMessageAccepted(notification.params.level)) {
+ logger.trace { "Filtering out logging message with level ${notification.params.level}" }
+ return
+ }
+ logger.trace { "Sending notification to client for session $sessionId: $notification" }
+ session.notification(notification, relatedRequestId)
+ }
+
+ override suspend fun ping(request: PingRequest, options: RequestOptions?): EmptyResult {
+ logger.trace { "Sending ping request: $request" }
+ return request(request, options)
+ }
+
+ override suspend fun createMessage(request: CreateMessageRequest, options: RequestOptions?): CreateMessageResult {
+ with(request.params) {
+ logger.debug {
+ "Creating message with ${messages.size} messages, maxTokens=$maxTokens, " +
+ "temperature=$temperature, " +
+ "systemPrompt=${if (systemPrompt != null) "present" else "absent"}"
+ }
+ }
+ logger.trace { "Full createMessage params: $request" }
+ return request(request, options)
+ }
+
+ override suspend fun listRoots(request: ListRootsRequest, options: RequestOptions?): ListRootsResult {
+ logger.debug { "Listing roots" }
+ return request(request, options)
+ }
+
+ override suspend fun createElicitation(request: ElicitRequest, options: RequestOptions?): ElicitResult {
+ with(request.params) {
+ logger.debug {
+ "Creating elicitation with message length=${message.length}, " +
+ "schema properties count=${requestedSchema.properties.size}"
+ }
+ }
+ logger.trace { "ElicitRequest: $request" }
+ return request(request, options)
+ }
+
+ override suspend fun createElicitation(
+ message: String,
+ requestedSchema: ElicitRequestParams.RequestedSchema,
+ options: RequestOptions?,
+ ): ElicitResult = createElicitation(
+ request = ElicitRequest(params = ElicitRequestParams(message, requestedSchema)),
+ options = options,
+ )
+
+ override suspend fun sendLoggingMessage(notification: LoggingMessageNotification) {
+ notification(notification)
+ }
+
+ override suspend fun sendResourceUpdated(notification: ResourceUpdatedNotification) {
+ logger.debug { "Sending resource updated notification for: ${notification.params.uri}" }
+ notification(notification)
+ }
+
+ override suspend fun sendResourceListChanged() {
+ logger.debug { "Sending resource list changed notification" }
+ notification(ResourceListChangedNotification())
+ }
+
+ override suspend fun sendToolListChanged() {
+ logger.debug { "Sending tool list changed notification" }
+ notification(ToolListChangedNotification())
+ }
+
+ override suspend fun sendPromptListChanged() {
+ logger.debug { "Sending prompt list changed notification" }
+ notification(PromptListChangedNotification())
+ }
+
+ /**
+ * Determines whether a message with the specified logging level is accepted
+ * based on the current logging level of the session.
+ *
+ * @param level The logging level of the message to be evaluated.
+ * @return True if the message is accepted (i.e., its level meets or exceeds
+ * the current session's logging level), or if no current logging
+ * level is set. Otherwise, false.
+ */
+ private fun isMessageAccepted(level: LoggingLevel): Boolean {
+ val current = session.currentLoggingLevel.value ?: return true // If no level is set, don't filter
+
+ return level >= current
+ }
+}
diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt
index 78bed9c..609aaa2 100644
--- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt
+++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt
@@ -25,7 +25,10 @@ internal interface Feature {
* @property tool The tool definition.
* @property handler A suspend function to handle the tool call requests.
*/
-public data class RegisteredTool(val tool: Tool, val handler: suspend (CallToolRequest) -> CallToolResult) : Feature {
+public data class RegisteredTool(
+ val tool: Tool,
+ val handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult,
+) : Feature {
override val key: String = tool.name
}
@@ -37,7 +40,7 @@ public data class RegisteredTool(val tool: Tool, val handler: suspend (CallToolR
*/
public data class RegisteredPrompt(
val prompt: Prompt,
- val messageProvider: suspend (GetPromptRequest) -> GetPromptResult,
+ val messageProvider: suspend ClientConnection.(GetPromptRequest) -> GetPromptResult,
) : Feature {
override val key: String = prompt.name
}
@@ -50,7 +53,7 @@ public data class RegisteredPrompt(
*/
public data class RegisteredResource(
val resource: Resource,
- val readHandler: suspend (ReadResourceRequest) -> ReadResourceResult,
+ val readHandler: suspend ClientConnection.(ReadResourceRequest) -> ReadResourceResult,
) : Feature {
override val key: String = resource.uri
}
diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt
index 1b035b4..2401709 100644
--- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt
+++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt
@@ -74,6 +74,7 @@ public class ServerOptions(public val capabilities: ServerCapabilities, enforceS
* this server. The provider is called each time a new session is started to support dynamic instructions.
* @param block A block to configure the mcp server.
*/
+@Suppress("TooManyFunctions")
public open class Server(
protected val serverInfo: Implementation,
protected val options: ServerOptions,
@@ -193,7 +194,7 @@ public open class Server(
handleListTools()
}
session.setRequestHandler(Method.Defined.ToolsCall) { request, _ ->
- handleCallTool(request)
+ handleCallTool(session, request)
}
}
@@ -203,7 +204,7 @@ public open class Server(
handleListPrompts()
}
session.setRequestHandler(Method.Defined.PromptsGet) { request, _ ->
- handleGetPrompt(request)
+ handleGetPrompt(session, request)
}
}
@@ -213,7 +214,7 @@ public open class Server(
handleListResources()
}
session.setRequestHandler(Method.Defined.ResourcesRead) { request, _ ->
- handleReadResource(request)
+ handleReadResource(session, request)
}
session.setRequestHandler(Method.Defined.ResourcesTemplatesList) { _, _ ->
handleListResourceTemplates()
@@ -294,7 +295,7 @@ public open class Server(
* @param handler A suspend function that handles executing the tool when called by the client.
* @throws IllegalStateException If the server does not support tools.
*/
- public fun addTool(tool: Tool, handler: suspend (CallToolRequest) -> CallToolResult) {
+ public fun addTool(tool: Tool, handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult) {
check(options.capabilities.tools != null) {
logger.error { "Failed to add tool '${tool.name}': Server does not support tools capability" }
"Server does not support tools capability. Enable it in ServerOptions."
@@ -316,6 +317,7 @@ public open class Server(
* @param handler A suspend function that handles executing the tool when called by the client.
* @throws IllegalStateException If the server does not support tools.
*/
+ @Suppress("LongParameterList")
public fun addTool(
name: String,
description: String,
@@ -324,7 +326,7 @@ public open class Server(
outputSchema: ToolSchema? = null,
toolAnnotations: ToolAnnotations? = null,
meta: JsonObject? = null,
- handler: suspend (CallToolRequest) -> CallToolResult,
+ handler: suspend ClientConnection.(CallToolRequest) -> CallToolResult,
) {
val tool = Tool(
name = name,
@@ -391,7 +393,10 @@ public open class Server(
* @param promptProvider A suspend function that returns the prompt content when requested by the client.
* @throws IllegalStateException If the server does not support prompts.
*/
- public fun addPrompt(prompt: Prompt, promptProvider: suspend (GetPromptRequest) -> GetPromptResult) {
+ public fun addPrompt(
+ prompt: Prompt,
+ promptProvider: suspend ClientConnection.(GetPromptRequest) -> GetPromptResult,
+ ) {
check(options.capabilities.prompts != null) {
logger.error { "Failed to add prompt '${prompt.name}': Server does not support prompts capability" }
"Server does not support prompts capability."
@@ -412,7 +417,7 @@ public open class Server(
name: String,
description: String? = null,
arguments: List? = null,
- promptProvider: suspend (GetPromptRequest) -> GetPromptResult,
+ promptProvider: suspend ClientConnection.(GetPromptRequest) -> GetPromptResult,
) {
val prompt = Prompt(name = name, description = description, arguments = arguments)
addPrompt(prompt, promptProvider)
@@ -479,7 +484,7 @@ public open class Server(
name: String,
description: String,
mimeType: String = "text/html",
- readHandler: suspend (ReadResourceRequest) -> ReadResourceResult,
+ readHandler: suspend ClientConnection.(ReadResourceRequest) -> ReadResourceResult,
) {
check(options.capabilities.resources != null) {
logger.error { "Failed to add resource '$name': Server does not support resources capability" }
@@ -557,7 +562,7 @@ public open class Server(
return ListToolsResult(tools = toolList, nextCursor = null)
}
- private suspend fun handleCallTool(request: CallToolRequest): CallToolResult {
+ private suspend fun handleCallTool(session: ServerSession, request: CallToolRequest): CallToolResult {
val requestParams = request.params
logger.debug { "Handling tool call request for tool: ${requestParams.name}" }
@@ -571,9 +576,12 @@ public open class Server(
}
// Execute the tool handler and catch any errors
+ @Suppress("TooGenericExceptionCaught")
return try {
logger.trace { "Executing tool ${requestParams.name} with input: ${requestParams.arguments}" }
- tool.handler(request)
+ tool.run {
+ session.clientConnection.handler(request)
+ }
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
@@ -590,7 +598,7 @@ public open class Server(
return ListPromptsResult(prompts = prompts.values.map { it.prompt })
}
- private suspend fun handleGetPrompt(request: GetPromptRequest): GetPromptResult {
+ private suspend fun handleGetPrompt(session: ServerSession, request: GetPromptRequest): GetPromptResult {
val requestParams = request.params
logger.debug { "Handling get prompt request for: ${requestParams.name}" }
val prompt = promptRegistry.get(requestParams.name)
@@ -598,7 +606,9 @@ public open class Server(
logger.error { "Prompt not found: ${requestParams.name}" }
throw IllegalArgumentException("Prompt not found: ${requestParams.name}")
}
- return prompt.messageProvider(request)
+ return prompt.run {
+ session.clientConnection.messageProvider(request)
+ }
}
private fun handleListResources(): ListResourcesResult {
@@ -606,7 +616,7 @@ public open class Server(
return ListResourcesResult(resources = resources.values.map { it.resource })
}
- private suspend fun handleReadResource(request: ReadResourceRequest): ReadResourceResult {
+ private suspend fun handleReadResource(session: ServerSession, request: ReadResourceRequest): ReadResourceResult {
val requestParams = request.params
logger.debug { "Handling read resource request for: ${requestParams.uri}" }
val resource = resourceRegistry.get(requestParams.uri)
@@ -614,7 +624,9 @@ public open class Server(
logger.error { "Resource not found: ${requestParams.uri}" }
throw IllegalArgumentException("Resource not found: ${requestParams.uri}")
}
- return resource.readHandler(request)
+ return resource.run {
+ session.clientConnection.readHandler(request)
+ }
}
private fun handleListResourceTemplates(): ListResourceTemplatesResult {
@@ -622,18 +634,19 @@ public open class Server(
return ListResourceTemplatesResult(listOf())
}
- // Start the ServerSession redirection section
+ // Start the ServerSession / ClientConnection redirection section
+
+ public fun clientConnection(sessionId: String): ClientConnection =
+ sessionRegistry.getSession(sessionId).clientConnection
/**
- * Triggers [ServerSession.ping] request for session by provided [sessionId].
+ * Triggers [ClientConnection.ping] request for session by provided [sessionId].
* @param sessionId The session ID to ping
*/
- public suspend fun ping(sessionId: String): EmptyResult = with(sessionRegistry.getSession(sessionId)) {
- ping()
- }
+ public suspend fun ping(sessionId: String): EmptyResult = clientConnection(sessionId).ping()
/**
- * Triggers [ServerSession.createMessage] request for session by provided [sessionId].
+ * Triggers [ClientConnection.createMessage] request for session by provided [sessionId].
*
* @param sessionId The session ID to create a message.
* @param params The parameters for creating a message.
@@ -645,12 +658,10 @@ public open class Server(
sessionId: String,
params: CreateMessageRequest,
options: RequestOptions? = null,
- ): CreateMessageResult = with(sessionRegistry.getSession(sessionId)) {
- request(params, options)
- }
+ ): CreateMessageResult = clientConnection(sessionId).createMessage(params, options)
/**
- * Triggers [ServerSession.listRoots] request for session by provided [sessionId].
+ * Triggers [ClientConnection.listRoots] request for session by provided [sessionId].
*
* @param sessionId The session ID to list roots for.
* @param params JSON parameters for the request, usually empty.
@@ -662,12 +673,10 @@ public open class Server(
sessionId: String,
params: JsonObject = EmptyJsonObject,
options: RequestOptions? = null,
- ): ListRootsResult = with(sessionRegistry.getSession(sessionId)) {
- listRoots(params, options)
- }
+ ): ListRootsResult = sessionRegistry.getSession(sessionId).listRoots(params, options)
/**
- * Triggers [ServerSession.createElicitation] request for session by provided [sessionId].
+ * Triggers [ClientConnection.createElicitation] request for session by provided [sessionId].
*
* @param sessionId The session ID to create elicitation for.
* @param message The elicitation message.
@@ -681,67 +690,59 @@ public open class Server(
message: String,
requestedSchema: ElicitRequestParams.RequestedSchema,
options: RequestOptions? = null,
- ): ElicitResult = with(sessionRegistry.getSession(sessionId)) {
- createElicitation(message, requestedSchema, options)
- }
+ ): ElicitResult = clientConnection(sessionId).createElicitation(
+ message = message,
+ requestedSchema = requestedSchema,
+ options = options,
+ )
/**
- * Triggers [ServerSession.sendLoggingMessage] for session by provided [sessionId].
+ * Triggers [ClientConnection.sendLoggingMessage] for session by provided [sessionId].
*
* @param sessionId The session ID to send the logging message to.
* @param notification The logging message notification.
*/
public suspend fun sendLoggingMessage(sessionId: String, notification: LoggingMessageNotification) {
- with(sessionRegistry.getSession(sessionId)) {
- sendLoggingMessage(notification)
- }
+ clientConnection(sessionId).sendLoggingMessage(notification)
}
/**
- * Triggers [ServerSession.sendResourceUpdated] for session by provided [sessionId].
+ * Triggers [ClientConnection.sendResourceUpdated] for session by provided [sessionId].
*
* @param sessionId The session ID to send the resource updated notification to.
* @param notification Details of the updated resource.
*/
public suspend fun sendResourceUpdated(sessionId: String, notification: ResourceUpdatedNotification) {
- with(sessionRegistry.getSession(sessionId)) {
- sendResourceUpdated(notification)
- }
+ clientConnection(sessionId).sendResourceUpdated(notification)
}
/**
- * Triggers [ServerSession.sendResourceListChanged] for session by provided [sessionId].
+ * Triggers [ClientConnection.sendResourceListChanged] for session by provided [sessionId].
*
* @param sessionId The session ID to send the resource list changed notification to.
*/
public suspend fun sendResourceListChanged(sessionId: String) {
- with(sessionRegistry.getSession(sessionId)) {
- sendResourceListChanged()
- }
+ clientConnection(sessionId).sendResourceListChanged()
}
/**
- * Triggers [ServerSession.sendToolListChanged] for session by provided [sessionId].
+ * Triggers [ClientConnection.sendToolListChanged] for session by provided [sessionId].
*
* @param sessionId The session ID to send the tool list changed notification to.
*/
public suspend fun sendToolListChanged(sessionId: String) {
- with(sessionRegistry.getSession(sessionId)) {
- sendToolListChanged()
- }
+ clientConnection(sessionId).sendToolListChanged()
}
/**
- * Triggers [ServerSession.sendPromptListChanged] for session by provided [sessionId].
+ * Triggers [ClientConnection.sendPromptListChanged] for session by provided [sessionId].
*
* @param sessionId The session ID to send the prompt list changed notification to.
*/
public suspend fun sendPromptListChanged(sessionId: String) {
- with(sessionRegistry.getSession(sessionId)) {
- sendPromptListChanged()
- }
+ clientConnection(sessionId).sendPromptListChanged()
}
- // End the ServerSession redirection section
+ // End the ServerSession / ClientConnection redirection section
// Start the notification handling section
public fun setNotificationHandler(method: Method, handler: (notification: T) -> Deferred) {
diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt
index ec20db2..69d20d1 100644
--- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt
+++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt
@@ -7,7 +7,6 @@ import io.modelcontextprotocol.kotlin.sdk.types.BaseRequestParams
import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest
import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult
-import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams
import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult
import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject
@@ -23,14 +22,10 @@ import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel
import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification
import io.modelcontextprotocol.kotlin.sdk.types.Method
import io.modelcontextprotocol.kotlin.sdk.types.Method.Defined
-import io.modelcontextprotocol.kotlin.sdk.types.PingRequest
-import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification
import io.modelcontextprotocol.kotlin.sdk.types.RequestMeta
-import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification
import io.modelcontextprotocol.kotlin.sdk.types.ResourceUpdatedNotification
import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS
import io.modelcontextprotocol.kotlin.sdk.types.SetLevelRequest
-import io.modelcontextprotocol.kotlin.sdk.types.ToolListChangedNotification
import kotlinx.atomicfu.AtomicRef
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CompletableDeferred
@@ -43,6 +38,7 @@ private val logger = KotlinLogging.logger {}
/**
* Represents a server session.
*/
+@Suppress("TooManyFunctions")
public open class ServerSession(
protected val serverInfo: Implementation,
options: ServerOptions,
@@ -77,7 +73,7 @@ public open class ServerSession(
* The current logging level set by the client.
* When null, all messages are sent (no filtering).
*/
- private val currentLoggingLevel: AtomicRef = atomic(null)
+ internal val currentLoggingLevel: AtomicRef = atomic(null)
init {
// Core protocol handlers
@@ -99,6 +95,8 @@ public open class ServerSession(
}
}
+ internal val clientConnection: ClientConnection = ClientConnectionImpl(this)
+
/**
* Registers a callback to be invoked when the server has completed initialization.
*/
@@ -129,114 +127,6 @@ public open class ServerSession(
_onClose()
}
- /**
- * Sends a ping request to the client to check connectivity.
- *
- * @return The result of the ping request.
- * @throws IllegalStateException If for some reason the method is not supported or the connection is closed.
- */
- public suspend fun ping(): EmptyResult = request(PingRequest())
-
- /**
- * Creates a message using the server's sampling capability.
- *
- * @param params The parameters for creating a message.
- * @param options Optional request options.
- * @return The created message result.
- * @throws IllegalStateException If the server does not support sampling or if the request fails.
- */
- public suspend fun createMessage(
- params: CreateMessageRequest,
- options: RequestOptions? = null,
- ): CreateMessageResult {
- logger.debug {
- "Creating message with ${params.params.messages.size} messages, maxTokens=${params.params.maxTokens}, temperature=${params.params.temperature}, systemPrompt=${if (params.params.systemPrompt != null) "present" else "absent"}"
- }
- logger.trace { "Full createMessage params: $params" }
- return request(params, options)
- }
-
- /**
- * Lists the available "roots" from the client's perspective (if supported).
- *
- * @param params JSON parameters for the request, usually empty.
- * @param options Optional request options.
- * @return The list of roots.
- * @throws IllegalStateException If the server or client does not support roots.
- */
- public suspend fun listRoots(
- params: JsonObject = EmptyJsonObject,
- options: RequestOptions? = null,
- ): ListRootsResult {
- logger.debug { "Listing roots with params: $params" }
- return request(ListRootsRequest(BaseRequestParams(RequestMeta(params))), options)
- }
-
- public suspend fun createElicitation(
- message: String,
- requestedSchema: ElicitRequestParams.RequestedSchema,
- options: RequestOptions? = null,
- ): ElicitResult {
- logger.debug {
- "Creating elicitation with message length=${message.length}, " +
- "schema properties count=${requestedSchema.properties.size}"
- }
- logger.trace { "Full elicitation message: $message, requestedSchema: $requestedSchema" }
- return request(ElicitRequest(ElicitRequestParams(message, requestedSchema)), options)
- }
-
- /**
- * Sends a logging message notification to the client.
- * Messages are filtered based on the current logging level set by the client.
- * If no logging level is set, all messages are sent.
- *
- * @param notification The logging message notification.
- */
- public suspend fun sendLoggingMessage(notification: LoggingMessageNotification) {
- if (serverCapabilities.logging != null) {
- if (isMessageAccepted(notification.params.level)) {
- logger.trace { "Sending logging message: ${notification.params.data}" }
- notification(notification)
- } else {
- logger.trace { "Filtering out logging message with level ${notification.params.level}" }
- }
- }
- }
-
- /**
- * Sends a resource-updated notification to the client, indicating that a specific resource has changed.
- *
- * @param notification Details of the updated resource.
- */
- public suspend fun sendResourceUpdated(notification: ResourceUpdatedNotification) {
- logger.debug { "Sending resource updated notification for: ${notification.params.uri}" }
- notification(notification)
- }
-
- /**
- * Sends a notification to the client indicating that the list of resources has changed.
- */
- public suspend fun sendResourceListChanged() {
- logger.debug { "Sending resource list changed notification" }
- notification(ResourceListChangedNotification())
- }
-
- /**
- * Sends a notification to the client indicating that the list of tools has changed.
- */
- public suspend fun sendToolListChanged() {
- logger.debug { "Sending tool list changed notification" }
- notification(ToolListChangedNotification())
- }
-
- /**
- * Sends a notification to the client indicating that the list of prompts has changed.
- */
- public suspend fun sendPromptListChanged() {
- logger.debug { "Sending prompt list changed notification" }
- notification(PromptListChangedNotification())
- }
-
/**
* Asserts that the client supports the capability required for the given [method].
*
@@ -251,21 +141,21 @@ public open class ServerSession(
Defined.SamplingCreateMessage -> {
if (clientCapabilities?.sampling == null) {
logger.error { "Client capability assertion failed: sampling not supported" }
- throw IllegalStateException("Client does not support sampling (required for ${method.value})")
+ error("Client does not support sampling (required for ${method.value})")
}
}
Defined.RootsList -> {
if (clientCapabilities?.roots == null) {
logger.error { "Client capability assertion failed: listing roots not supported" }
- throw IllegalStateException("Client does not support listing roots (required for ${method.value})")
+ error("Client does not support listing roots (required for ${method.value})")
}
}
Defined.ElicitationCreate -> {
if (clientCapabilities?.elicitation == null) {
logger.error { "Client capability assertion failed: elicitation not supported" }
- throw IllegalStateException("Client does not support elicitation (required for ${method.value})")
+ error("Client does not support elicitation (required for ${method.value})")
}
}
@@ -292,7 +182,7 @@ public open class ServerSession(
Defined.NotificationsMessage -> {
if (serverCapabilities.logging == null) {
logger.error { "Server capability assertion failed: logging not supported" }
- throw IllegalStateException("Server does not support logging (required for ${method.value})")
+ error("Server does not support logging (required for ${method.value})")
}
}
@@ -300,7 +190,7 @@ public open class ServerSession(
Defined.NotificationsResourcesListChanged,
-> {
if (serverCapabilities.resources == null) {
- throw IllegalStateException(
+ error(
"Server does not support notifying about resources (required for ${method.value})",
)
}
@@ -308,7 +198,7 @@ public open class ServerSession(
Defined.NotificationsToolsListChanged -> {
if (serverCapabilities.tools == null) {
- throw IllegalStateException(
+ error(
"Server does not support notifying of tool list changes (required for ${method.value})",
)
}
@@ -316,7 +206,7 @@ public open class ServerSession(
Defined.NotificationsPromptsListChanged -> {
if (serverCapabilities.prompts == null) {
- throw IllegalStateException(
+ error(
"Server does not support notifying of prompt list changes (required for ${method.value})",
)
}
@@ -347,14 +237,14 @@ public open class ServerSession(
Defined.SamplingCreateMessage -> {
if (serverCapabilities.experimental?.get("sampling") == null) {
logger.error { "Server capability assertion failed: sampling not supported" }
- throw IllegalStateException("Server does not support sampling (required for $method)")
+ error("Server does not support sampling (required for $method)")
}
}
Defined.LoggingSetLevel -> {
if (serverCapabilities.logging == null) {
logger.error { "Server does not support logging (required for $method)" }
- throw IllegalStateException("Server does not support logging (required for $method)")
+ error("Server does not support logging (required for $method)")
}
}
@@ -362,7 +252,7 @@ public open class ServerSession(
Defined.PromptsList,
-> {
if (serverCapabilities.prompts == null) {
- throw IllegalStateException("Server does not support prompts (required for $method)")
+ error("Server does not support prompts (required for $method)")
}
}
@@ -373,7 +263,7 @@ public open class ServerSession(
Defined.ResourcesUnsubscribe,
-> {
if (serverCapabilities.resources == null) {
- throw IllegalStateException("Server does not support resources (required for $method)")
+ error("Server does not support resources (required for $method)")
}
}
@@ -381,7 +271,7 @@ public open class ServerSession(
Defined.ToolsList,
-> {
if (serverCapabilities.tools == null) {
- throw IllegalStateException("Server does not support tools (required for $method)")
+ error("Server does not support tools (required for $method)")
}
}
@@ -395,7 +285,7 @@ public open class ServerSession(
}
}
- private suspend fun handleInitialize(request: InitializeRequest): InitializeResult {
+ private fun handleInitialize(request: InitializeRequest): InitializeResult {
logger.debug { "Handling initialization request from client" }
clientCapabilities = request.params.capabilities
clientVersion = request.params.clientInfo
@@ -405,7 +295,8 @@ public open class ServerSession(
requestedVersion
} else {
logger.warn {
- "Client requested unsupported protocol version $requestedVersion, falling back to $LATEST_PROTOCOL_VERSION"
+ "Client requested unsupported protocol version $requestedVersion, " +
+ "falling back to $LATEST_PROTOCOL_VERSION"
}
LATEST_PROTOCOL_VERSION
}
@@ -418,26 +309,6 @@ public open class ServerSession(
)
}
- /**
- * Checks if a message with the given level should be ignored based on the current logging level.
- *
- * @param level The level of the message to check.
- * @return true if the message should be ignored (filtered out), false otherwise.
- */
- private fun isMessageIgnored(level: LoggingLevel): Boolean {
- val current = currentLoggingLevel.value ?: return false // If no level is set, don't filter
-
- return level < current
- }
-
- /**
- * Checks if a message with the given level should be accepted based on the current logging level.
- *
- * @param level The level of the message to check.
- * @return true if the message should be accepted (not filtered out), false otherwise.
- */
- private fun isMessageAccepted(level: LoggingLevel): Boolean = !isMessageIgnored(level)
-
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ServerSession) return false
@@ -445,4 +316,94 @@ public open class ServerSession(
}
override fun hashCode(): Int = sessionId.hashCode()
+
+ // Start the ClientConnection redirection section
+
+ /**
+ * Sends a ping request to the client to check connectivity.
+ *
+ * @return The result of the ping request.
+ * @throws IllegalStateException If for some reason the method is not supported or the connection is closed.
+ */
+ public suspend fun ping(): EmptyResult = clientConnection.ping()
+
+ /**
+ * Creates a message using the server's sampling capability.
+ *
+ * @param params The parameters for creating a message.
+ * @param options Optional request options.
+ * @return The created message result.
+ * @throws IllegalStateException If the server does not support sampling or if the request fails.
+ */
+ public suspend fun createMessage(
+ params: CreateMessageRequest,
+ options: RequestOptions? = null,
+ ): CreateMessageResult = clientConnection.createMessage(params, options)
+
+ /**
+ * Lists the available "roots" from the client's perspective (if supported).
+ *
+ * @param params JSON parameters for the request, usually empty.
+ * @param options Optional request options.
+ * @return The list of roots.
+ * @throws IllegalStateException If the server or client does not support roots.
+ */
+ public suspend fun listRoots(
+ params: JsonObject = EmptyJsonObject,
+ options: RequestOptions? = null,
+ ): ListRootsResult {
+ logger.debug { "Listing roots with params: $params" }
+ return request(ListRootsRequest(BaseRequestParams(RequestMeta(params))), options)
+ }
+
+ /**
+ * Sends a message to the client requesting an elicitation.
+ * This typically results in a form being displayed to the end user.
+ *
+ * @param message The message for the elicitation to display.
+ * @param requestedSchema The schema requested by the client for the elicitation result.
+ * Influences the form displayed to the user.
+ * @param options Optional request options.
+ * @return The result of the elicitation request.
+ * @throws IllegalStateException If the server or client does not support elicitation.
+ */
+ public suspend fun createElicitation(
+ message: String,
+ requestedSchema: ElicitRequestParams.RequestedSchema,
+ options: RequestOptions? = null,
+ ): ElicitResult = clientConnection.createElicitation(message, requestedSchema, options)
+
+ /**
+ * Sends a logging message notification to the client.
+ * Messages are filtered based on the current logging level set by the client.
+ * If no logging level is set, all messages are sent.
+ *
+ * @param notification The logging message notification.
+ */
+ public suspend fun sendLoggingMessage(notification: LoggingMessageNotification): Unit =
+ clientConnection.sendLoggingMessage(notification)
+
+ /**
+ * Sends a resource-updated notification to the client, indicating that a specific resource has changed.
+ *
+ * @param notification Details of the updated resource.
+ */
+ public suspend fun sendResourceUpdated(notification: ResourceUpdatedNotification): Unit =
+ clientConnection.sendResourceUpdated(notification)
+
+ /**
+ * Sends a notification to the client indicating that the list of resources has changed.
+ */
+ public suspend fun sendResourceListChanged(): Unit = clientConnection.sendResourceListChanged()
+
+ /**
+ * Sends a notification to the client indicating that the list of tools has changed.
+ */
+ public suspend fun sendToolListChanged(): Unit = clientConnection.sendToolListChanged()
+
+ /**
+ * Sends a notification to the client indicating that the list of prompts has changed.
+ */
+ public suspend fun sendPromptListChanged(): Unit = clientConnection.sendPromptListChanged()
+ // End the ClientConnection redirection section
}