Add support for elicitation (#138)
* feat(client): add support for elicitation Fixes #137 * feat(client): add a convenience method to set the elicitation handler
This commit is contained in:
parent
609b52cf63
commit
675cc73c6d
4 changed files with 320 additions and 5 deletions
|
|
@ -8,6 +8,8 @@ import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities
|
|||
import io.modelcontextprotocol.kotlin.sdk.CompatibilityCallToolResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.CompleteRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.CompleteResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateElicitationResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.EmptyRequestResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.GetPromptResult
|
||||
|
|
@ -262,6 +264,14 @@ public open class Client(
|
|||
}
|
||||
}
|
||||
|
||||
Method.Defined.ElicitationCreate -> {
|
||||
if (capabilities.elicitation == null) {
|
||||
throw IllegalStateException(
|
||||
"Client does not support elicitation capability (required for $method)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Method.Defined.Ping -> {
|
||||
// No capability required
|
||||
}
|
||||
|
|
@ -571,6 +581,24 @@ public open class Client(
|
|||
notification(RootsListChangedNotification())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the elicitation handler.
|
||||
*
|
||||
* @param handler The elicitation handler.
|
||||
* @throws IllegalStateException if the client does not support elicitation.
|
||||
*/
|
||||
public fun setElicitationHandler(handler: (CreateElicitationRequest) -> CreateElicitationResult) {
|
||||
if (capabilities.elicitation == null) {
|
||||
logger.error { "Failed to set elicitation handler: Client does not support elicitation" }
|
||||
throw IllegalStateException("Client does not support elicitation.")
|
||||
}
|
||||
logger.info { "Setting the elicitation handler" }
|
||||
|
||||
setRequestHandler<CreateElicitationRequest>(Method.Defined.ElicitationCreate) { request, _ ->
|
||||
handler(request)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Internal Handlers ---
|
||||
|
||||
private suspend fun handleListRoots(): ListRootsResult {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
|||
import io.modelcontextprotocol.kotlin.sdk.CallToolRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.CallToolResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.ClientCapabilities
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateElicitationRequest.RequestedSchema
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateElicitationResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateMessageRequest
|
||||
import io.modelcontextprotocol.kotlin.sdk.CreateMessageResult
|
||||
import io.modelcontextprotocol.kotlin.sdk.EmptyJsonObject
|
||||
|
|
@ -525,6 +528,15 @@ public open class Server(
|
|||
return request<ListRootsResult>(ListRootsRequest(params), options)
|
||||
}
|
||||
|
||||
public suspend fun createElicitation(
|
||||
message: String,
|
||||
requestedSchema: RequestedSchema,
|
||||
options: RequestOptions? = null
|
||||
): CreateElicitationResult {
|
||||
logger.debug { "Creating elicitation with message: $message" }
|
||||
return request(CreateElicitationRequest(message, requestedSchema), options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a logging message notification to the client.
|
||||
*
|
||||
|
|
@ -666,6 +678,12 @@ public open class Server(
|
|||
}
|
||||
}
|
||||
|
||||
"elicitation/create" -> {
|
||||
if (clientCapabilities?.elicitation == null) {
|
||||
throw IllegalStateException("Client does not support elicitation (required for ${method.value})")
|
||||
}
|
||||
}
|
||||
|
||||
"ping" -> {
|
||||
// No specific capability required
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,8 @@ public sealed interface Method {
|
|||
LoggingSetLevel("logging/setLevel"),
|
||||
SamplingCreateMessage("sampling/createMessage"),
|
||||
CompletionComplete("completion/complete"),
|
||||
RootsList("roots/list")
|
||||
RootsList("roots/list"),
|
||||
ElicitationCreate("elicitation/create"),
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -337,6 +338,10 @@ public data class ClientCapabilities(
|
|||
* Present if the client supports listing roots.
|
||||
*/
|
||||
val roots: Roots? = null,
|
||||
/**
|
||||
* Present if the client supports elicitation.
|
||||
*/
|
||||
val elicitation: JsonObject? = null,
|
||||
) {
|
||||
@Serializable
|
||||
public data class Roots(
|
||||
|
|
@ -1544,6 +1549,48 @@ public class RootsListChangedNotification : ClientNotification {
|
|||
override val method: Method = Method.Defined.NotificationsRootsListChanged
|
||||
}
|
||||
|
||||
/**
|
||||
* Sent from the server to create an elicitation from the client.
|
||||
*/
|
||||
@Serializable
|
||||
public data class CreateElicitationRequest(
|
||||
public val message: String,
|
||||
public val requestedSchema: RequestedSchema,
|
||||
override val _meta: JsonObject = EmptyJsonObject,
|
||||
) : ServerRequest, WithMeta {
|
||||
override val method: Method = Method.Defined.ElicitationCreate
|
||||
|
||||
@Serializable
|
||||
public data class RequestedSchema(
|
||||
val properties: JsonObject = EmptyJsonObject,
|
||||
val required: List<String>? = null,
|
||||
) {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@EncodeDefault
|
||||
val type: String = "object"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The client's response to an elicitation/create request from the server.
|
||||
*/
|
||||
@Serializable
|
||||
public data class CreateElicitationResult(
|
||||
public val action: Action,
|
||||
public val content: JsonObject? = null,
|
||||
override val _meta: JsonObject = EmptyJsonObject,
|
||||
) : ClientResult {
|
||||
|
||||
init {
|
||||
require(action == Action.accept || content == null) {
|
||||
"Content can only be provided for an 'accept' action"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
public enum class Action { accept, decline, cancel }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an error specific to the MCP protocol.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package client
|
|||
import io.mockk.coEvery
|
||||
import io.mockk.spyk
|
||||
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
|
||||
|
|
@ -42,6 +44,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertInstanceOf
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
|
|
@ -796,4 +799,106 @@ class ClientTest {
|
|||
"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 = assertThrows<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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue