From 4a0895912197ab8036bedba8dffd35f2ebf15502 Mon Sep 17 00:00:00 2001 From: Ryan Nett Date: Mon, 23 Feb 2026 06:30:15 -0800 Subject: [PATCH] feat! Add context to server call handlers (#515) ## Motivation and Context Adds a `ClientConnection` context receiver to server call handlers, which provides access to the client communication channel. This is necessary to allow tools to send notifications, logging, elicitations, etc. Supersedes https://github.com/modelcontextprotocol/kotlin-sdk/pull/144, discussed in https://github.com/modelcontextprotocol/kotlin-sdk/issues/295. ## How Has This Been Tested? With the added integration test. ## Breaking Changes It breaks binary compatibility and breaks source compatibility for tool handlers that already use `this`. If you care a lot about maintaining compatibility, I could add some overloads to ease the transition. But the new methods would likely need to be named differently for resolution. Future compatibility should be easier to maintain - `Context` is an interface (with experimental opt-in subclassing), so new fields can be added without breaking compatibility. Different types could also be introduced for different handles without breaking compatibility, as long as they extend `ClientConnection`. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context --------- Co-authored-by: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> --- integration-test/detekt-baseline-test.xml | 1 - .../sdk/server/AbstractServerFeaturesTest.kt | 33 +++ .../sdk/server/ClientConnectionLoggingTest.kt | 172 ++++++++++++ .../kotlin/sdk/server/ClientConnectionTest.kt | 163 +++++++++++ .../sdk/server/ServerBulkFeaturesTest.kt | 183 +++++++++++++ .../server/ServerFeaturesInvocationTest.kt | 222 +++++++++++++++ .../sdk/server/ServerFeaturesListTest.kt | 221 +++++++++++++++ .../server/ServerHandlerNotificationTest.kt | 124 +++++++++ kotlin-sdk-server/api/kotlin-sdk-server.api | 77 ++++-- .../detekt-baseline-commonMainSourceSet.xml | 8 - kotlin-sdk-server/detekt-baseline-main.xml | 22 -- .../kotlin/sdk/server/ClientConnection.kt | 249 +++++++++++++++++ .../kotlin/sdk/server/Feature.kt | 9 +- .../kotlin/sdk/server/Server.kt | 105 +++---- .../kotlin/sdk/server/ServerSession.kt | 257 ++++++++---------- 15 files changed, 1589 insertions(+), 257 deletions(-) create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionLoggingTest.kt create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerBulkFeaturesTest.kt create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesInvocationTest.kt create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerFeaturesListTest.kt create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerHandlerNotificationTest.kt create mode 100644 kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt 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 }