modularize sdk (#208)
* Modularize SDK: migrated client and server implementations to separate modules. Removed old configuration files and reorganized the source code structure * add support native targets and update js/wasm configurations * Refactor sample projects: add samples as composite builds, replace hardcoded dependencies with version catalog, remove obsolete gradle wrapper files * Update .gitignore: exclude SWE agent directories (.claude/ and .junie/) * Refine CodeQL workflow: update Kotlin build steps to target individual modules separately instead of building all at once
This commit is contained in:
parent
641df74f5d
commit
8698b96dd2
10 changed files with 979 additions and 2003 deletions
|
|
@ -0,0 +1,894 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.client
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateElicitationResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateMessageRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateMessageResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.EmptyJsonObject
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializeRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializeResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCResponse
|
||||
import io.modelcontextprotocol.kotlin.sdk.LATEST_PROTOCOL_VERSION
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListResourcesRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListResourcesResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListRootsRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListToolsRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListToolsResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.LoggingLevel
|
||||
import io.modelcontextprotocol.kotlin.sdk.LoggingMessageNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.Method
|
||||
import io.modelcontextprotocol.kotlin.sdk.Role
|
||||
import io.modelcontextprotocol.kotlin.sdk.Root
|
||||
import io.modelcontextprotocol.kotlin.sdk.RootsListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.SUPPORTED_PROTOCOL_VERSIONS
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.Tool
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.Server
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
class ClientTest {
|
||||
@Test
|
||||
fun `should initialize with matching protocol version`() = runTest {
|
||||
var initialised = false
|
||||
val clientTransport = object : AbstractTransport() {
|
||||
override suspend fun start() {}
|
||||
|
||||
override suspend fun send(message: JSONRPCMessage) {
|
||||
if (message !is JSONRPCRequest) return
|
||||
initialised = true
|
||||
val result = InitializeResult(
|
||||
protocolVersion = LATEST_PROTOCOL_VERSION,
|
||||
capabilities = ServerCapabilities(),
|
||||
serverInfo = Implementation(
|
||||
name = "test",
|
||||
version = "1.0"
|
||||
)
|
||||
)
|
||||
|
||||
val response = JSONRPCResponse(
|
||||
id = message.id,
|
||||
result = result
|
||||
)
|
||||
|
||||
_onMessage.invoke(response)
|
||||
}
|
||||
|
||||
override suspend fun close() {
|
||||
}
|
||||
}
|
||||
|
||||
val client = Client(
|
||||
clientInfo = Implementation(
|
||||
name = "test client",
|
||||
version = "1.0"
|
||||
),
|
||||
options = ClientOptions(
|
||||
capabilities = ClientCapabilities(
|
||||
sampling = EmptyJsonObject
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
client.connect(clientTransport)
|
||||
assertTrue(initialised)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should initialize with supported older protocol version`() = runTest {
|
||||
val OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]
|
||||
val clientTransport = object : AbstractTransport() {
|
||||
override suspend fun start() {}
|
||||
|
||||
override suspend fun send(message: JSONRPCMessage) {
|
||||
if (message !is JSONRPCRequest) return
|
||||
check(message.method == Method.Defined.Initialize.value)
|
||||
|
||||
val result = InitializeResult(
|
||||
protocolVersion = OLD_VERSION,
|
||||
capabilities = ServerCapabilities(),
|
||||
serverInfo = Implementation(
|
||||
name = "test",
|
||||
version = "1.0"
|
||||
)
|
||||
)
|
||||
|
||||
val response = JSONRPCResponse(
|
||||
id = message.id,
|
||||
result = result
|
||||
)
|
||||
_onMessage.invoke(response)
|
||||
}
|
||||
|
||||
override suspend fun close() {
|
||||
}
|
||||
}
|
||||
|
||||
val client = Client(
|
||||
clientInfo = Implementation(
|
||||
name = "test client",
|
||||
version = "1.0"
|
||||
),
|
||||
options = ClientOptions(
|
||||
capabilities = ClientCapabilities(
|
||||
sampling = EmptyJsonObject
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
client.connect(clientTransport)
|
||||
assertEquals(
|
||||
Implementation("test", "1.0"),
|
||||
client.serverVersion
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject unsupported protocol version`() = runTest {
|
||||
var closed = false
|
||||
val clientTransport = object : AbstractTransport() {
|
||||
override suspend fun start() {}
|
||||
|
||||
override suspend fun send(message: JSONRPCMessage) {
|
||||
if (message !is JSONRPCRequest) return
|
||||
check(message.method == Method.Defined.Initialize.value)
|
||||
|
||||
val result = InitializeResult(
|
||||
protocolVersion = "invalid-version",
|
||||
capabilities = ServerCapabilities(),
|
||||
serverInfo = Implementation(
|
||||
name = "test",
|
||||
version = "1.0"
|
||||
)
|
||||
)
|
||||
|
||||
val response = JSONRPCResponse(
|
||||
id = message.id,
|
||||
result = result
|
||||
)
|
||||
|
||||
_onMessage.invoke(response)
|
||||
}
|
||||
|
||||
override suspend fun close() {
|
||||
closed = true
|
||||
}
|
||||
}
|
||||
|
||||
val client = Client(
|
||||
clientInfo = Implementation(
|
||||
name = "test client",
|
||||
version = "1.0"
|
||||
),
|
||||
options = ClientOptions()
|
||||
)
|
||||
|
||||
assertFailsWith<IllegalStateException>("Server's protocol version is not supported: invalid-version") {
|
||||
client.connect(clientTransport)
|
||||
}
|
||||
|
||||
assertTrue(closed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject due to non cancellation exception`() = runTest {
|
||||
var closed = false
|
||||
val failingTransport = object : AbstractTransport() {
|
||||
override suspend fun start() {}
|
||||
|
||||
override suspend fun send(message: JSONRPCMessage) {
|
||||
if (message !is JSONRPCRequest) return
|
||||
check(message.method == Method.Defined.Initialize.value)
|
||||
throw IllegalStateException("Test error")
|
||||
}
|
||||
|
||||
override suspend fun close() {
|
||||
closed = true
|
||||
}
|
||||
}
|
||||
|
||||
val client = Client(
|
||||
clientInfo = Implementation(
|
||||
name = "test client",
|
||||
version = "1.0"
|
||||
),
|
||||
options = ClientOptions()
|
||||
)
|
||||
|
||||
val exception = assertFailsWith<IllegalStateException> {
|
||||
client.connect(failingTransport)
|
||||
}
|
||||
|
||||
assertEquals("Error connecting to transport: Test error", exception.message)
|
||||
|
||||
assertTrue(closed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should respect server capabilities`() = runTest {
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
resources = ServerCapabilities.Resources(null, null),
|
||||
tools = ServerCapabilities.Tools(null)
|
||||
)
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
server.setRequestHandler<InitializeRequest>(Method.Defined.Initialize) { _, _ ->
|
||||
InitializeResult(
|
||||
protocolVersion = LATEST_PROTOCOL_VERSION,
|
||||
capabilities = ServerCapabilities(
|
||||
resources = ServerCapabilities.Resources(null, null),
|
||||
tools = ServerCapabilities.Tools(null)
|
||||
),
|
||||
serverInfo = Implementation(name = "test", version = "1.0")
|
||||
)
|
||||
}
|
||||
|
||||
server.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { _, _ ->
|
||||
ListResourcesResult(resources = emptyList(), nextCursor = null)
|
||||
}
|
||||
|
||||
server.setRequestHandler<ListToolsRequest>(Method.Defined.ToolsList) { _, _ ->
|
||||
ListToolsResult(tools = emptyList(), nextCursor = null)
|
||||
}
|
||||
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
options = ClientOptions(
|
||||
capabilities = ClientCapabilities(sampling = EmptyJsonObject),
|
||||
)
|
||||
)
|
||||
|
||||
listOf(
|
||||
launch {
|
||||
client.connect(clientTransport)
|
||||
},
|
||||
launch {
|
||||
server.connect(serverTransport)
|
||||
}
|
||||
).joinAll()
|
||||
|
||||
// Server supports resources and tools, but not prompts
|
||||
val caps = client.serverCapabilities
|
||||
assertEquals(ServerCapabilities.Resources(null, null), caps?.resources)
|
||||
assertEquals(ServerCapabilities.Tools(null), caps?.tools)
|
||||
assertTrue(caps?.prompts == null) // or check that prompts are absent
|
||||
|
||||
// These should not throw
|
||||
client.listResources()
|
||||
client.listTools()
|
||||
|
||||
// This should fail because prompts are not supported
|
||||
val ex = assertFailsWith<IllegalStateException> {
|
||||
client.listPrompts()
|
||||
}
|
||||
assertTrue(ex.message?.contains("Server does not support prompts") == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should respect client notification capabilities`() = runTest {
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
ServerOptions(capabilities = ServerCapabilities())
|
||||
)
|
||||
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
options = ClientOptions(
|
||||
capabilities = ClientCapabilities(
|
||||
roots = ClientCapabilities.Roots(listChanged = true)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
|
||||
listOf(
|
||||
launch {
|
||||
client.connect(clientTransport)
|
||||
println("Client connected")
|
||||
},
|
||||
launch {
|
||||
server.connect(serverTransport)
|
||||
println("Server connected")
|
||||
}
|
||||
).joinAll()
|
||||
|
||||
// This should not throw because the client supports roots.listChanged
|
||||
client.sendRootsListChanged()
|
||||
|
||||
// Create a new client without the roots.listChanged capability
|
||||
val clientWithoutCapability = Client(
|
||||
clientInfo = Implementation(name = "test client without capability", version = "1.0"),
|
||||
options = ClientOptions(
|
||||
capabilities = ClientCapabilities(),
|
||||
// enforceStrictCapabilities = true // TODO()
|
||||
)
|
||||
)
|
||||
|
||||
clientWithoutCapability.connect(clientTransport)
|
||||
// Using the same transport pair might not be realistic - in a real scenario you'd create another pair.
|
||||
// Adjust if necessary.
|
||||
|
||||
// This should fail
|
||||
val ex = assertFailsWith<IllegalStateException> {
|
||||
clientWithoutCapability.sendRootsListChanged()
|
||||
}
|
||||
assertTrue(ex.message?.startsWith("Client does not support") == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should respect server notification capabilities`() = runTest {
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
logging = EmptyJsonObject,
|
||||
resources = ServerCapabilities.Resources(listChanged = true, subscribe = null)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
options = ClientOptions(
|
||||
capabilities = ClientCapabilities()
|
||||
)
|
||||
)
|
||||
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
|
||||
listOf(
|
||||
launch {
|
||||
client.connect(clientTransport)
|
||||
println("Client connected")
|
||||
},
|
||||
launch {
|
||||
server.connect(serverTransport)
|
||||
println("Server connected")
|
||||
}
|
||||
).joinAll()
|
||||
|
||||
// These should not throw
|
||||
val jsonObject = buildJsonObject {
|
||||
put("name", "John")
|
||||
put("age", 30)
|
||||
put("isStudent", false)
|
||||
}
|
||||
server.sendLoggingMessage(
|
||||
LoggingMessageNotification(
|
||||
params = LoggingMessageNotification.Params(
|
||||
level = LoggingLevel.info,
|
||||
data = jsonObject
|
||||
)
|
||||
)
|
||||
)
|
||||
server.sendResourceListChanged()
|
||||
|
||||
// This should fail because the server doesn't have the tools capability
|
||||
val ex = assertFailsWith<IllegalStateException> {
|
||||
server.sendToolListChanged()
|
||||
}
|
||||
assertTrue(ex.message?.contains("Server does not support notifying of tool list changes") == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle client cancelling a request`() = runTest {
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
resources = ServerCapabilities.Resources(
|
||||
listChanged = null,
|
||||
subscribe = null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val def = CompletableDeferred<Unit>()
|
||||
val defTimeOut = CompletableDeferred<Unit>()
|
||||
server.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { _, _ ->
|
||||
// Simulate delay
|
||||
def.complete(Unit)
|
||||
try {
|
||||
delay(1000)
|
||||
} catch (e: CancellationException) {
|
||||
defTimeOut.complete(Unit)
|
||||
throw e
|
||||
}
|
||||
ListResourcesResult(resources = emptyList())
|
||||
fail("Shouldn't have been called")
|
||||
}
|
||||
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
options = ClientOptions(capabilities = ClientCapabilities())
|
||||
)
|
||||
|
||||
listOf(
|
||||
launch {
|
||||
client.connect(clientTransport)
|
||||
println("Client connected")
|
||||
},
|
||||
launch {
|
||||
server.connect(serverTransport)
|
||||
println("Server connected")
|
||||
}
|
||||
).joinAll()
|
||||
|
||||
val defCancel = CompletableDeferred<Unit>()
|
||||
val job = launch {
|
||||
try {
|
||||
client.listResources()
|
||||
} catch (e: CancellationException) {
|
||||
defCancel.complete(Unit)
|
||||
assertEquals("Cancelled by test", e.message)
|
||||
}
|
||||
}
|
||||
def.await()
|
||||
runCatching { job.cancel("Cancelled by test") }
|
||||
defCancel.await()
|
||||
defTimeOut.await()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle request timeout`() = runTest {
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
resources = ServerCapabilities.Resources(
|
||||
listChanged = null,
|
||||
subscribe = null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
server.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { _, _ ->
|
||||
// Simulate a delayed response
|
||||
// Wait ~100ms unless canceled
|
||||
try {
|
||||
withTimeout(100L) {
|
||||
// Just delay here, if timeout is 0 on the client side, this won't return in time
|
||||
delay(100)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// If aborted, just rethrow or return early
|
||||
}
|
||||
ListResourcesResult(resources = emptyList())
|
||||
}
|
||||
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
options = ClientOptions(capabilities = ClientCapabilities())
|
||||
)
|
||||
|
||||
listOf(
|
||||
launch {
|
||||
client.connect(clientTransport)
|
||||
println("Client connected")
|
||||
},
|
||||
launch {
|
||||
server.connect(serverTransport)
|
||||
println("Server connected")
|
||||
}
|
||||
).joinAll()
|
||||
|
||||
// Request with 1 msec timeout should fail immediately
|
||||
val ex = assertFailsWith<Exception> {
|
||||
withTimeout(1) {
|
||||
client.listResources()
|
||||
}
|
||||
}
|
||||
assertTrue(ex is TimeoutCancellationException)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should only allow setRequestHandler for declared capabilities`() = runTest {
|
||||
val client = Client(
|
||||
clientInfo = Implementation(
|
||||
name = "test client",
|
||||
version = "1.0"
|
||||
),
|
||||
options = ClientOptions(
|
||||
capabilities = ClientCapabilities(
|
||||
sampling = EmptyJsonObject
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
client.setRequestHandler<CreateMessageRequest>(Method.Defined.SamplingCreateMessage) { _, _ ->
|
||||
CreateMessageResult(
|
||||
model = "test-model",
|
||||
role = Role.assistant,
|
||||
content = TextContent(
|
||||
text = "Test response"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
assertFailsWith<IllegalStateException>("Client does not support roots capability (required for RootsList)") {
|
||||
client.setRequestHandler<ListRootsRequest>(Method.Defined.RootsList) { _, _ -> null }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `JSONRPCRequest with ToolsList method and default params returns list of tools`() = runTest {
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
tools = ServerCapabilities.Tools(null)
|
||||
)
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
server.setRequestHandler<InitializeRequest>(Method.Defined.Initialize) { _, _ ->
|
||||
InitializeResult(
|
||||
protocolVersion = LATEST_PROTOCOL_VERSION,
|
||||
capabilities = ServerCapabilities(
|
||||
resources = ServerCapabilities.Resources(null, null),
|
||||
tools = ServerCapabilities.Tools(null)
|
||||
),
|
||||
serverInfo = Implementation(name = "test", version = "1.0")
|
||||
)
|
||||
}
|
||||
val serverListToolsResult = ListToolsResult(
|
||||
tools = listOf(
|
||||
Tool(
|
||||
name = "testTool",
|
||||
title = "testTool title",
|
||||
description = "testTool description",
|
||||
annotations = null,
|
||||
inputSchema = Tool.Input(),
|
||||
outputSchema = null
|
||||
)
|
||||
), nextCursor = null
|
||||
)
|
||||
|
||||
server.setRequestHandler<ListToolsRequest>(Method.Defined.ToolsList) { _, _ ->
|
||||
serverListToolsResult
|
||||
}
|
||||
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
options = ClientOptions(
|
||||
capabilities = ClientCapabilities(sampling = EmptyJsonObject),
|
||||
)
|
||||
)
|
||||
|
||||
var receivedMessage: JSONRPCMessage? = null
|
||||
clientTransport.onMessage { msg ->
|
||||
receivedMessage = msg
|
||||
}
|
||||
|
||||
listOf(
|
||||
launch {
|
||||
client.connect(clientTransport)
|
||||
},
|
||||
launch {
|
||||
server.connect(serverTransport)
|
||||
}
|
||||
).joinAll()
|
||||
|
||||
val serverCapabilities = client.serverCapabilities
|
||||
assertEquals(ServerCapabilities.Tools(null), serverCapabilities?.tools)
|
||||
|
||||
val request = JSONRPCRequest(
|
||||
method = Method.Defined.ToolsList.value
|
||||
)
|
||||
clientTransport.send(request)
|
||||
|
||||
assertIs<JSONRPCResponse>(receivedMessage)
|
||||
val receivedAsResponse = receivedMessage as JSONRPCResponse
|
||||
assertEquals(request.id, receivedAsResponse.id)
|
||||
assertEquals(request.jsonrpc, receivedAsResponse.jsonrpc)
|
||||
assertEquals(serverListToolsResult, receivedAsResponse.result)
|
||||
assertEquals(null, receivedAsResponse.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `listRoots returns list of roots`() = runTest {
|
||||
val client = Client(
|
||||
Implementation(name = "test client", version = "1.0"),
|
||||
ClientOptions(
|
||||
capabilities = ClientCapabilities(
|
||||
roots = ClientCapabilities.Roots(null)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val clientRoots = listOf(
|
||||
Root(uri = "file:///test-root", name = "testRoot")
|
||||
)
|
||||
|
||||
client.addRoots(clientRoots)
|
||||
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
|
||||
val server = Server(
|
||||
serverInfo = Implementation(name = "test server", version = "1.0"),
|
||||
options = ServerOptions(
|
||||
capabilities = ServerCapabilities()
|
||||
)
|
||||
)
|
||||
|
||||
listOf(
|
||||
launch { client.connect(clientTransport) },
|
||||
launch { server.connect(serverTransport) }
|
||||
).joinAll()
|
||||
|
||||
val clientCapabilities = server.clientCapabilities
|
||||
assertEquals(ClientCapabilities.Roots(null), clientCapabilities?.roots)
|
||||
|
||||
val listRootsResult = server.listRoots()
|
||||
|
||||
assertEquals(listRootsResult.roots, clientRoots)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `addRoot should throw when roots capability is not supported`() = runTest {
|
||||
val client = Client(
|
||||
Implementation(name = "test client", version = "1.0"),
|
||||
ClientOptions(
|
||||
capabilities = ClientCapabilities()
|
||||
)
|
||||
)
|
||||
|
||||
// Verify that adding a root throws an exception
|
||||
val exception = assertFailsWith<IllegalStateException> {
|
||||
client.addRoot(uri = "file:///test-root1", name = "testRoot1")
|
||||
}
|
||||
assertEquals("Client does not support roots capability.", exception.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeRoot should throw when roots capability is not supported`() = runTest {
|
||||
val client = Client(
|
||||
Implementation(name = "test client", version = "1.0"),
|
||||
ClientOptions(
|
||||
capabilities = ClientCapabilities()
|
||||
)
|
||||
)
|
||||
|
||||
// Verify that removing a root throws an exception
|
||||
val exception = assertFailsWith<IllegalStateException> {
|
||||
client.removeRoot(uri = "file:///test-root1")
|
||||
}
|
||||
assertEquals("Client does not support roots capability.", exception.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeRoot should remove a root`() = runTest {
|
||||
val client = Client(
|
||||
Implementation(name = "test client", version = "1.0"),
|
||||
ClientOptions(
|
||||
capabilities = ClientCapabilities(
|
||||
roots = ClientCapabilities.Roots(null)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Add some roots
|
||||
client.addRoots(
|
||||
listOf(
|
||||
Root(uri = "file:///test-root1", name = "testRoot1"),
|
||||
Root(uri = "file:///test-root2", name = "testRoot2"),
|
||||
)
|
||||
)
|
||||
|
||||
// Remove a root
|
||||
val result = client.removeRoot("file:///test-root1")
|
||||
|
||||
// Verify the root was removed
|
||||
assertTrue(result, "Root should be removed successfully")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeRoots should remove multiple roots`() = runTest {
|
||||
val client = Client(
|
||||
Implementation(name = "test client", version = "1.0"),
|
||||
ClientOptions(
|
||||
capabilities = ClientCapabilities(
|
||||
roots = ClientCapabilities.Roots(null)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Add some roots
|
||||
client.addRoots(
|
||||
listOf(
|
||||
Root(uri = "file:///test-root1", name = "testRoot1"),
|
||||
Root(uri = "file:///test-root2", name = "testRoot2"),
|
||||
)
|
||||
)
|
||||
|
||||
// Remove multiple roots
|
||||
val result = client.removeRoots(
|
||||
listOf("file:///test-root1", "file:///test-root2")
|
||||
)
|
||||
|
||||
// Verify the root was removed
|
||||
assertEquals(2, result, "Both roots should be removed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendRootsListChanged should notify server`() = runTest {
|
||||
val client = Client(
|
||||
Implementation(name = "test client", version = "1.0"),
|
||||
ClientOptions(
|
||||
capabilities = ClientCapabilities(
|
||||
roots = ClientCapabilities.Roots(listChanged = true)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
|
||||
val server = Server(
|
||||
serverInfo = Implementation(name = "test server", version = "1.0"),
|
||||
options = ServerOptions(
|
||||
capabilities = ServerCapabilities()
|
||||
)
|
||||
)
|
||||
|
||||
// Track notifications
|
||||
var rootListChangedNotificationReceived = false
|
||||
server.setNotificationHandler<RootsListChangedNotification>(Method.Defined.NotificationsRootsListChanged) {
|
||||
rootListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
listOf(
|
||||
launch { client.connect(clientTransport) },
|
||||
launch { server.connect(serverTransport) }
|
||||
).joinAll()
|
||||
|
||||
client.sendRootsListChanged()
|
||||
|
||||
assertTrue(
|
||||
rootListChangedNotificationReceived,
|
||||
"Notification should be sent when sendRootsListChanged is called"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject server elicitation when elicitation capability is not supported`() = runTest {
|
||||
val client = Client(
|
||||
Implementation(name = "test client", version = "1.0"),
|
||||
ClientOptions(
|
||||
capabilities = ClientCapabilities()
|
||||
)
|
||||
)
|
||||
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
|
||||
val server = Server(
|
||||
serverInfo = Implementation(name = "test server", version = "1.0"),
|
||||
options = ServerOptions(
|
||||
capabilities = ServerCapabilities()
|
||||
)
|
||||
)
|
||||
|
||||
listOf(
|
||||
launch { client.connect(clientTransport) },
|
||||
launch { server.connect(serverTransport) }
|
||||
).joinAll()
|
||||
|
||||
// Verify that creating an elicitation throws an exception
|
||||
val exception = assertFailsWith<IllegalStateException> {
|
||||
server.createElicitation(
|
||||
message = "Please provide your GitHub username",
|
||||
requestedSchema = CreateElicitationRequest.RequestedSchema(
|
||||
properties = buildJsonObject {
|
||||
putJsonObject("name") {
|
||||
put("type", "string")
|
||||
}
|
||||
},
|
||||
required = listOf("name")
|
||||
)
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
"Client does not support elicitation (required for elicitation/create)",
|
||||
exception.message
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle server elicitation`() = runTest {
|
||||
val client = Client(
|
||||
Implementation(name = "test client", version = "1.0"),
|
||||
ClientOptions(
|
||||
capabilities = ClientCapabilities(
|
||||
elicitation = EmptyJsonObject,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val elicitationMessage = "Please provide your GitHub username"
|
||||
val requestedSchema = CreateElicitationRequest.RequestedSchema(
|
||||
properties = buildJsonObject {
|
||||
putJsonObject("name") {
|
||||
put("type", "string")
|
||||
}
|
||||
},
|
||||
required = listOf("name")
|
||||
)
|
||||
|
||||
val elicitationResultAction = CreateElicitationResult.Action.accept
|
||||
val elicitationResultContent = buildJsonObject {
|
||||
put("name", "octocat")
|
||||
}
|
||||
|
||||
client.setElicitationHandler { request ->
|
||||
assertEquals(elicitationMessage, request.message)
|
||||
assertEquals(requestedSchema, request.requestedSchema)
|
||||
|
||||
CreateElicitationResult(
|
||||
action = elicitationResultAction,
|
||||
content = elicitationResultContent
|
||||
)
|
||||
}
|
||||
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
|
||||
val server = Server(
|
||||
serverInfo = Implementation(name = "test server", version = "1.0"),
|
||||
options = ServerOptions(
|
||||
capabilities = ServerCapabilities()
|
||||
)
|
||||
)
|
||||
|
||||
listOf(
|
||||
launch { client.connect(clientTransport) },
|
||||
launch { server.connect(serverTransport) }
|
||||
).joinAll()
|
||||
|
||||
val result = server.createElicitation(
|
||||
message = elicitationMessage,
|
||||
requestedSchema = requestedSchema
|
||||
)
|
||||
|
||||
assertEquals(elicitationResultAction, result.action)
|
||||
assertEquals(elicitationResultContent, result.content)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.client
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.cio.CIO
|
||||
import io.ktor.server.engine.EmbeddedServer
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.routing.routing
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.Server
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.mcp
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import io.ktor.client.plugins.sse.SSE as ClientSSE
|
||||
import io.ktor.server.sse.SSE as ServerSSE
|
||||
|
||||
class SseTransportTest : BaseTransportTest() {
|
||||
|
||||
private suspend fun EmbeddedServer<*, *>.actualPort() = engine.resolvedConnectors().first().port
|
||||
|
||||
private lateinit var mcpServer: Server
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
mcpServer = Server(
|
||||
serverInfo = Implementation(
|
||||
name = "test-server",
|
||||
version = "1.0"
|
||||
),
|
||||
options = ServerOptions(ServerCapabilities())
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should start then close cleanly`() = runTest {
|
||||
val server = embeddedServer(CIO, port = 0) {
|
||||
install(ServerSSE)
|
||||
routing {
|
||||
mcp { mcpServer }
|
||||
}
|
||||
}.startSuspend(wait = false)
|
||||
|
||||
val actualPort = server.actualPort()
|
||||
|
||||
val client = HttpClient {
|
||||
install(ClientSSE)
|
||||
}.mcpSseTransport {
|
||||
url {
|
||||
host = "localhost"
|
||||
this.port = actualPort
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
testClientOpenClose(client)
|
||||
} finally {
|
||||
server.stopSuspend()
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
fun `should read messages`() = runTest {
|
||||
val server = embeddedServer(CIO, port = 0) {
|
||||
install(ServerSSE)
|
||||
routing {
|
||||
mcp { mcpServer }
|
||||
// sse {
|
||||
// mcpSseTransport("", transports).apply {
|
||||
// onMessage {
|
||||
// send(it)
|
||||
// }
|
||||
//
|
||||
// start()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// post {
|
||||
// mcpPostEndpoint(transports)
|
||||
// }
|
||||
}
|
||||
}.startSuspend(wait = false)
|
||||
|
||||
val actualPort = server.actualPort()
|
||||
|
||||
val client = HttpClient {
|
||||
install(ClientSSE)
|
||||
}.mcpSseTransport {
|
||||
url {
|
||||
host = "localhost"
|
||||
this.port = actualPort
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
testClientRead(client)
|
||||
} finally {
|
||||
server.stopSuspend()
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
fun `test sse path not root path`() = runTest {
|
||||
val server = embeddedServer(CIO, port = 0) {
|
||||
install(ServerSSE)
|
||||
routing {
|
||||
mcp("/sse") { mcpServer }
|
||||
// route("/sse") {
|
||||
// sse {
|
||||
// mcpSseTransport("", transports).apply {
|
||||
// onMessage {
|
||||
// send(it)
|
||||
// }
|
||||
//
|
||||
// start()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// post {
|
||||
// mcpPostEndpoint(transports)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}.startSuspend(wait = false)
|
||||
|
||||
val actualPort = server.actualPort()
|
||||
|
||||
val client = HttpClient {
|
||||
install(ClientSSE)
|
||||
}.mcpSseTransport {
|
||||
url {
|
||||
host = "localhost"
|
||||
this.port = actualPort
|
||||
pathSegments = listOf("sse")
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
testClientRead(client)
|
||||
} finally {
|
||||
server.stopSuspend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.client
|
||||
|
||||
import io.ktor.server.testing.testApplication
|
||||
import io.ktor.server.websocket.WebSockets
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.mcpWebSocket
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.mcpWebSocketTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
|
||||
class WebSocketTransportTest : BaseTransportTest() {
|
||||
@Test
|
||||
@Ignore // "Test disabled for investigation #17"
|
||||
fun `should start then close cleanly`() = testApplication {
|
||||
install(WebSockets)
|
||||
routing {
|
||||
mcpWebSocket()
|
||||
}
|
||||
|
||||
val client = createClient {
|
||||
install(io.ktor.client.plugins.websocket.WebSockets)
|
||||
}.mcpWebSocketTransport()
|
||||
|
||||
testClientOpenClose(client)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // "Test disabled for investigation #17"
|
||||
fun `should read messages`() = testApplication {
|
||||
val clientFinished = CompletableDeferred<Unit>()
|
||||
|
||||
install(WebSockets)
|
||||
routing {
|
||||
mcpWebSocketTransport {
|
||||
onMessage {
|
||||
send(it)
|
||||
}
|
||||
|
||||
clientFinished.await()
|
||||
}
|
||||
}
|
||||
|
||||
val client = createClient {
|
||||
install(io.ktor.client.plugins.websocket.WebSockets)
|
||||
}.mcpWebSocketTransport()
|
||||
|
||||
testClientRead(client)
|
||||
|
||||
clientFinished.complete(Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport
|
||||
import io.modelcontextprotocol.kotlin.sdk.toJSON
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class InMemoryTransportTest {
|
||||
private lateinit var clientTransport: InMemoryTransport
|
||||
private lateinit var serverTransport: InMemoryTransport
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
val (client, server) = InMemoryTransport.createLinkedPair()
|
||||
clientTransport = client
|
||||
serverTransport = server
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should create linked pair`() {
|
||||
assertNotNull(clientTransport)
|
||||
assertNotNull(serverTransport)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should start without error`() = runTest {
|
||||
clientTransport.start()
|
||||
serverTransport.start()
|
||||
// If no exception is thrown, the test passes
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should send message from client to server`() = runTest {
|
||||
val message = InitializedNotification()
|
||||
|
||||
var receivedMessage: JSONRPCMessage? = null
|
||||
serverTransport.onMessage { msg ->
|
||||
receivedMessage = msg
|
||||
}
|
||||
|
||||
val rpcNotification = message.toJSON()
|
||||
clientTransport.send(rpcNotification)
|
||||
assertEquals(rpcNotification, receivedMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should send message from server to client`() = runTest {
|
||||
val message = InitializedNotification()
|
||||
.toJSON()
|
||||
|
||||
var receivedMessage: JSONRPCMessage? = null
|
||||
clientTransport.onMessage { msg ->
|
||||
receivedMessage = msg
|
||||
}
|
||||
|
||||
serverTransport.send(message)
|
||||
assertEquals(message, receivedMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle close`() = runTest {
|
||||
var clientClosed = false
|
||||
var serverClosed = false
|
||||
|
||||
clientTransport.onClose {
|
||||
clientClosed = true
|
||||
}
|
||||
|
||||
serverTransport.onClose {
|
||||
serverClosed = true
|
||||
}
|
||||
|
||||
clientTransport.close()
|
||||
assertTrue(clientClosed)
|
||||
assertTrue(serverClosed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should throw error when sending after close`() = runTest {
|
||||
clientTransport.close()
|
||||
|
||||
assertFailsWith<IllegalStateException> {
|
||||
clientTransport.send(
|
||||
InitializedNotification().toJSON()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should queue messages sent before start`() = runTest {
|
||||
val message = InitializedNotification()
|
||||
.toJSON()
|
||||
|
||||
var receivedMessage: JSONRPCMessage? = null
|
||||
serverTransport.onMessage { msg ->
|
||||
receivedMessage = msg
|
||||
}
|
||||
|
||||
clientTransport.send(message)
|
||||
serverTransport.start()
|
||||
assertEquals(message, receivedMessage)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.integration
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.sse.SSE
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.cio.CIOApplicationEngine
|
||||
import io.ktor.server.engine.EmbeddedServer
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.routing.routing
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.client.Client
|
||||
import io.modelcontextprotocol.kotlin.sdk.client.mcpSse
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.Server
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.mcp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.fail
|
||||
import io.ktor.client.engine.cio.CIO as ClientCIO
|
||||
import io.ktor.server.cio.CIO as ServerCIO
|
||||
|
||||
class SseIntegrationTest {
|
||||
@Test
|
||||
fun `client should be able to connect to sse server`() = runTest {
|
||||
val serverEngine = initServer()
|
||||
var client: Client? = null
|
||||
try {
|
||||
withContext(Dispatchers.Default) {
|
||||
assertDoesNotThrow { client = initClient() }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
fail("Failed to connect client: $e")
|
||||
} finally {
|
||||
client?.close()
|
||||
// Make sure to stop the server
|
||||
serverEngine.stopSuspend(1000, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> assertDoesNotThrow(block: () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
fail("Expected no exception, but got: $e")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initClient(): Client {
|
||||
return HttpClient(ClientCIO) { install(SSE) }.mcpSse("http://$URL:$PORT")
|
||||
}
|
||||
|
||||
private suspend fun initServer(): EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration> {
|
||||
val server = Server(
|
||||
Implementation(name = "sse-e2e-test", version = "1.0.0"),
|
||||
ServerOptions(capabilities = ServerCapabilities()),
|
||||
)
|
||||
|
||||
return embeddedServer(ServerCIO, host = URL, port = PORT) {
|
||||
install(io.ktor.server.sse.SSE)
|
||||
routing {
|
||||
mcp { server }
|
||||
}
|
||||
}.startSuspend(wait = false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PORT = 3001
|
||||
private const val URL = "localhost"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.shared
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.InitializedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
import io.modelcontextprotocol.kotlin.sdk.PingRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.toJSON
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
abstract class BaseTransportTest {
|
||||
protected suspend fun testClientOpenClose(client: Transport) {
|
||||
client.onError { error ->
|
||||
fail("Unexpected error: $error")
|
||||
}
|
||||
|
||||
var didClose = false
|
||||
client.onClose { didClose = true }
|
||||
|
||||
client.start()
|
||||
assertFalse(didClose, "Transport should not be closed immediately after start")
|
||||
|
||||
client.close()
|
||||
assertTrue(didClose, "Transport should be closed after close() call")
|
||||
}
|
||||
|
||||
protected suspend fun testClientRead(client: Transport) {
|
||||
client.onError { error ->
|
||||
error.printStackTrace()
|
||||
fail("Unexpected error: $error")
|
||||
}
|
||||
|
||||
val messages = listOf<JSONRPCMessage>(
|
||||
PingRequest().toJSON(), InitializedNotification().toJSON()
|
||||
)
|
||||
|
||||
val readMessages = mutableListOf<JSONRPCMessage>()
|
||||
val finished = CompletableDeferred<Unit>()
|
||||
|
||||
client.onMessage { message ->
|
||||
readMessages.add(message)
|
||||
if (message == messages.last()) {
|
||||
finished.complete(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
client.start()
|
||||
|
||||
for (message in messages) {
|
||||
client.send(message)
|
||||
}
|
||||
|
||||
finished.await()
|
||||
|
||||
assertEquals(messages, readMessages, "Assert messages received")
|
||||
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.shared
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
|
||||
|
||||
/**
|
||||
* In-memory transport for creating clients and servers that talk to each other within the same process.
|
||||
*/
|
||||
class InMemoryTransport : AbstractTransport() {
|
||||
private var otherTransport: InMemoryTransport? = null
|
||||
private val messageQueue: MutableList<JSONRPCMessage> = mutableListOf()
|
||||
|
||||
/**
|
||||
* Creates a pair of linked in-memory transports that can communicate with each other.
|
||||
* One should be passed to a Client and one to a Server.
|
||||
*/
|
||||
companion object {
|
||||
fun createLinkedPair(): Pair<InMemoryTransport, InMemoryTransport> {
|
||||
val clientTransport = InMemoryTransport()
|
||||
val serverTransport = InMemoryTransport()
|
||||
clientTransport.otherTransport = serverTransport
|
||||
serverTransport.otherTransport = clientTransport
|
||||
return Pair(clientTransport, serverTransport)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun start() {
|
||||
// Process any messages that were queued before start was called
|
||||
while (messageQueue.isNotEmpty()) {
|
||||
messageQueue.removeFirstOrNull()?.let { message ->
|
||||
_onMessage.invoke(message) // todo?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun close() {
|
||||
val other = otherTransport
|
||||
otherTransport = null
|
||||
other?.close()
|
||||
_onClose.invoke()
|
||||
}
|
||||
|
||||
override suspend fun send(message: JSONRPCMessage) {
|
||||
val other = otherTransport ?: throw IllegalStateException("Not connected")
|
||||
|
||||
other._onMessage.invoke(message)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.client
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.ListToolsResult
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.io.asSink
|
||||
import kotlinx.io.asSource
|
||||
import kotlinx.io.buffered
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.net.Socket
|
||||
|
||||
class ClientIntegrationTest {
|
||||
|
||||
fun createTransport(): StdioClientTransport {
|
||||
val socket = Socket("localhost", 3000)
|
||||
|
||||
return StdioClientTransport(
|
||||
socket.inputStream.asSource().buffered(),
|
||||
socket.outputStream.asSink().buffered()
|
||||
)
|
||||
}
|
||||
|
||||
@Disabled("This test requires a running server")
|
||||
@Test
|
||||
fun testRequestTools() = runTest {
|
||||
val client = Client(
|
||||
Implementation("test", "1.0"),
|
||||
)
|
||||
|
||||
val transport = createTransport()
|
||||
try {
|
||||
client.connect(transport)
|
||||
|
||||
val response: ListToolsResult = client.listTools()
|
||||
println(response.tools)
|
||||
|
||||
} finally {
|
||||
transport.close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.client
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.io.asSink
|
||||
import kotlinx.io.asSource
|
||||
import kotlinx.io.buffered
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class StdioClientTransportTest : BaseTransportTest() {
|
||||
@Test
|
||||
fun `should start then close cleanly`() = runTest {
|
||||
// Run process "/usr/bin/tee"
|
||||
val processBuilder = ProcessBuilder("/usr/bin/tee")
|
||||
val process = processBuilder.start()
|
||||
|
||||
val input = process.inputStream.asSource().buffered()
|
||||
val output = process.outputStream.asSink().buffered()
|
||||
|
||||
val client = StdioClientTransport(
|
||||
input = input,
|
||||
output = output
|
||||
)
|
||||
|
||||
testClientOpenClose(client)
|
||||
|
||||
process.destroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should read messages`() = runTest {
|
||||
val processBuilder = ProcessBuilder("/usr/bin/tee")
|
||||
val process = processBuilder.start()
|
||||
|
||||
val input = process.inputStream.asSource().buffered()
|
||||
val output = process.outputStream.asSink().buffered()
|
||||
|
||||
val client = StdioClientTransport(
|
||||
input = input,
|
||||
output = output
|
||||
)
|
||||
|
||||
testClientRead(client)
|
||||
|
||||
process.waitFor()
|
||||
process.destroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,472 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.server
|
||||
|
||||
import io.modelcontextprotocol.kotlin.sdk.CallToolResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.Implementation
|
||||
import io.modelcontextprotocol.kotlin.sdk.Method
|
||||
import io.modelcontextprotocol.kotlin.sdk.Prompt
|
||||
import io.modelcontextprotocol.kotlin.sdk.PromptListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ResourceListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.TextContent
|
||||
import io.modelcontextprotocol.kotlin.sdk.TextResourceContents
|
||||
import io.modelcontextprotocol.kotlin.sdk.Tool
|
||||
import io.modelcontextprotocol.kotlin.sdk.ToolListChangedNotification
|
||||
import io.modelcontextprotocol.kotlin.sdk.client.Client
|
||||
import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ServerTest {
|
||||
|
||||
@Test
|
||||
fun `removeTool should remove a tool`() = runTest {
|
||||
// Create server with tools capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
tools = ServerCapabilities.Tools(null)
|
||||
)
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
// Add a tool
|
||||
server.addTool("test-tool", "Test Tool", Tool.Input()) {
|
||||
CallToolResult(listOf(TextContent("Test result")))
|
||||
}
|
||||
|
||||
// Setup client
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
)
|
||||
|
||||
// Connect client and server
|
||||
launch { client.connect(clientTransport) }
|
||||
launch { server.connect(serverTransport) }
|
||||
|
||||
// Remove the tool
|
||||
val result = server.removeTool("test-tool")
|
||||
|
||||
// Verify the tool was removed
|
||||
assertTrue(result, "Tool should be removed successfully")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeTool should return false when tool does not exist`() = runTest {
|
||||
// Create server with tools capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
tools = ServerCapabilities.Tools(null)
|
||||
)
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
// Setup client
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
)
|
||||
|
||||
// Track notifications
|
||||
var toolListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<ToolListChangedNotification>(Method.Defined.NotificationsToolsListChanged) {
|
||||
toolListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Connect client and server
|
||||
launch { client.connect(clientTransport) }
|
||||
launch { server.connect(serverTransport) }
|
||||
|
||||
// Try to remove a non-existent tool
|
||||
val result = server.removeTool("non-existent-tool")
|
||||
|
||||
// Verify the result
|
||||
assertFalse(result, "Removing non-existent tool should return false")
|
||||
assertFalse(toolListChangedNotificationReceived, "No notification should be sent when tool doesn't exist")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeTool should throw when tools capability is not supported`() = runTest {
|
||||
// Create server without tools capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities()
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
// Verify that removing a tool throws an exception
|
||||
val exception = assertThrows<IllegalStateException> {
|
||||
server.removeTool("test-tool")
|
||||
}
|
||||
assertEquals("Server does not support tools capability.", exception.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeTools should remove multiple tools`() = runTest {
|
||||
// Create server with tools capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
tools = ServerCapabilities.Tools(null)
|
||||
)
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
// Add tools
|
||||
server.addTool("test-tool-1", "Test Tool 1") {
|
||||
CallToolResult(listOf(TextContent("Test result 1")))
|
||||
}
|
||||
server.addTool("test-tool-2", "Test Tool 2") {
|
||||
CallToolResult(listOf(TextContent("Test result 2")))
|
||||
}
|
||||
|
||||
// Setup client
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
)
|
||||
|
||||
// Connect client and server
|
||||
launch { client.connect(clientTransport) }
|
||||
launch { server.connect(serverTransport) }
|
||||
|
||||
// Remove the tools
|
||||
val result = server.removeTools(listOf("test-tool-1", "test-tool-2"))
|
||||
|
||||
// Verify the tools were removed
|
||||
assertEquals(2, result, "Both tools should be removed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removePrompt should remove a prompt`() = runTest {
|
||||
// Create server with prompts capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
prompts = ServerCapabilities.Prompts(listChanged = false)
|
||||
)
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
// Add a prompt
|
||||
val testPrompt = Prompt("test-prompt", "Test Prompt", null)
|
||||
server.addPrompt(testPrompt) {
|
||||
GetPromptResult(
|
||||
description = "Test prompt description",
|
||||
messages = listOf()
|
||||
)
|
||||
}
|
||||
|
||||
// Setup client
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
)
|
||||
|
||||
// Connect client and server
|
||||
launch { client.connect(clientTransport) }
|
||||
launch { server.connect(serverTransport) }
|
||||
|
||||
// Remove the prompt
|
||||
val result = server.removePrompt(testPrompt.name)
|
||||
|
||||
// Verify the prompt was removed
|
||||
assertTrue(result, "Prompt should be removed successfully")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removePrompts should remove multiple prompts and send notification`() = runTest {
|
||||
// Create server with prompts capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
prompts = ServerCapabilities.Prompts(listChanged = false)
|
||||
)
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
// Add prompts
|
||||
val testPrompt1 = Prompt("test-prompt-1", "Test Prompt 1", null)
|
||||
val testPrompt2 = Prompt("test-prompt-2", "Test Prompt 2", null)
|
||||
server.addPrompt(testPrompt1) {
|
||||
GetPromptResult(
|
||||
description = "Test prompt description 1",
|
||||
messages = listOf()
|
||||
)
|
||||
}
|
||||
server.addPrompt(testPrompt2) {
|
||||
GetPromptResult(
|
||||
description = "Test prompt description 2",
|
||||
messages = listOf()
|
||||
)
|
||||
}
|
||||
|
||||
// Setup client
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
)
|
||||
|
||||
// Connect client and server
|
||||
launch { client.connect(clientTransport) }
|
||||
launch { server.connect(serverTransport) }
|
||||
|
||||
// Remove the prompts
|
||||
val result = server.removePrompts(listOf(testPrompt1.name, testPrompt2.name))
|
||||
|
||||
// Verify the prompts were removed
|
||||
assertEquals(2, result, "Both prompts should be removed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeResource should remove a resource and send notification`() = runTest {
|
||||
// Create server with resources capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
resources = ServerCapabilities.Resources(null, null)
|
||||
)
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
// Add a resource
|
||||
val testResourceUri = "test://resource"
|
||||
server.addResource(
|
||||
uri = testResourceUri,
|
||||
name = "Test Resource",
|
||||
description = "A test resource",
|
||||
mimeType = "text/plain"
|
||||
) {
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = "Test resource content",
|
||||
uri = testResourceUri,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Setup client
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
)
|
||||
|
||||
// Connect client and server
|
||||
launch { client.connect(clientTransport) }
|
||||
launch { server.connect(serverTransport) }
|
||||
|
||||
// Remove the resource
|
||||
val result = server.removeResource(testResourceUri)
|
||||
|
||||
// Verify the resource was removed
|
||||
assertTrue(result, "Resource should be removed successfully")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeResources should remove multiple resources and send notification`() = runTest {
|
||||
// Create server with resources capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
resources = ServerCapabilities.Resources(null, null)
|
||||
)
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
// Add resources
|
||||
val testResourceUri1 = "test://resource1"
|
||||
val testResourceUri2 = "test://resource2"
|
||||
server.addResource(
|
||||
uri = testResourceUri1,
|
||||
name = "Test Resource 1",
|
||||
description = "A test resource 1",
|
||||
mimeType = "text/plain"
|
||||
) {
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = "Test resource content 1",
|
||||
uri = testResourceUri1,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
server.addResource(
|
||||
uri = testResourceUri2,
|
||||
name = "Test Resource 2",
|
||||
description = "A test resource 2",
|
||||
mimeType = "text/plain"
|
||||
) {
|
||||
ReadResourceResult(
|
||||
contents = listOf(
|
||||
TextResourceContents(
|
||||
text = "Test resource content 2",
|
||||
uri = testResourceUri2,
|
||||
mimeType = "text/plain",
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Setup client
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
)
|
||||
|
||||
// Connect client and server
|
||||
launch { client.connect(clientTransport) }
|
||||
launch { server.connect(serverTransport) }
|
||||
|
||||
// Remove the resources
|
||||
val result = server.removeResources(listOf(testResourceUri1, testResourceUri2))
|
||||
|
||||
// Verify the resources were removed
|
||||
assertEquals(2, result, "Both resources should be removed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removePrompt should return false when prompt does not exist`() = runTest {
|
||||
// Create server with prompts capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
prompts = ServerCapabilities.Prompts(listChanged = false)
|
||||
)
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
// Setup client
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
)
|
||||
|
||||
// Track notifications
|
||||
var promptListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<PromptListChangedNotification>(Method.Defined.NotificationsPromptsListChanged) {
|
||||
promptListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Connect client and server
|
||||
launch { client.connect(clientTransport) }
|
||||
launch { server.connect(serverTransport) }
|
||||
|
||||
// Try to remove a non-existent prompt
|
||||
val result = server.removePrompt("non-existent-prompt")
|
||||
|
||||
// Verify the result
|
||||
assertFalse(result, "Removing non-existent prompt should return false")
|
||||
assertFalse(promptListChangedNotificationReceived, "No notification should be sent when prompt doesn't exist")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removePrompt should throw when prompts capability is not supported`() = runTest {
|
||||
// Create server without prompts capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities()
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
// Verify that removing a prompt throws an exception
|
||||
val exception = assertThrows<IllegalStateException> {
|
||||
server.removePrompt("test-prompt")
|
||||
}
|
||||
assertEquals("Server does not support prompts capability.", exception.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeResource should return false when resource does not exist`() = runTest {
|
||||
// Create server with resources capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities(
|
||||
resources = ServerCapabilities.Resources(null, null)
|
||||
)
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
// Setup client
|
||||
val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
|
||||
val client = Client(
|
||||
clientInfo = Implementation(name = "test client", version = "1.0"),
|
||||
)
|
||||
|
||||
// Track notifications
|
||||
var resourceListChangedNotificationReceived = false
|
||||
client.setNotificationHandler<ResourceListChangedNotification>(Method.Defined.NotificationsResourcesListChanged) {
|
||||
resourceListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
// Connect client and server
|
||||
launch { client.connect(clientTransport) }
|
||||
launch { server.connect(serverTransport) }
|
||||
|
||||
// Try to remove a non-existent resource
|
||||
val result = server.removeResource("non-existent-resource")
|
||||
|
||||
// Verify the result
|
||||
assertFalse(result, "Removing non-existent resource should return false")
|
||||
assertFalse(
|
||||
resourceListChangedNotificationReceived,
|
||||
"No notification should be sent when resource doesn't exist"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeResource should throw when resources capability is not supported`() = runTest {
|
||||
// Create server without resources capability
|
||||
val serverOptions = ServerOptions(
|
||||
capabilities = ServerCapabilities()
|
||||
)
|
||||
val server = Server(
|
||||
Implementation(name = "test server", version = "1.0"),
|
||||
serverOptions
|
||||
)
|
||||
|
||||
// Verify that removing a resource throws an exception
|
||||
val exception = assertThrows<IllegalStateException> {
|
||||
server.removeResource("test://resource")
|
||||
}
|
||||
assertEquals("Server does not support resources capability.", exception.message)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue