feat: add URL mode elicitation and typed schema definitions per MCP (#660)

Add URL mode elicitation support and typed `PrimitiveSchemaDefinition`
hierarchy to align with MCP specification 2025-11-25

closes #540 

## How Has This Been Tested?
unit/integration/conformance

## Breaking Changes
- `ElicitRequestParams` changed from `data class` to `sealed interface`.
A deprecated factory function preserves source compatibility for
`ElicitRequestParams(message, requestedSchema)`.
- `RequestedSchema.properties` changed from `JsonObject` to `Map<String,
PrimitiveSchemaDefinition>`.
- `ElicitRequest.requestedSchema` is deprecated, use `(params as
ElicitRequestFormParams).requestedSchema`.

## 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
This commit is contained in:
Pavel Gorgulov 2026-04-01 12:20:12 +02:00 • committed by GitHub
commit fdecf6bdf5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1838 additions and 384 deletions

View file

@ -5,7 +5,9 @@ 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.ElicitRequestFormParams
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestURLParams
import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult
import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult
import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest
@ -104,6 +106,24 @@ public interface ClientConnection {
options: RequestOptions? = null,
): ElicitResult
/**
* Sends a URL mode elicitation request to the client, directing the user
* to an external URL for out-of-band interactions.
*
* @param message The message explaining why the interaction is needed.
* @param elicitationId A unique identifier for the elicitation.
* @param url The URL that the user should navigate to.
* @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,
elicitationId: String,
url: String,
options: RequestOptions? = null,
): ElicitResult
/**
* Sends a message to the client requesting elicitation.
* This typically results in a form being displayed to the user.
@ -189,10 +209,16 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client
}
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}"
val params = request.params
logger.debug {
when (params) {
is ElicitRequestFormParams ->
"Creating form elicitation with message length=${params.message.length}, " +
"schema properties count=${params.requestedSchema.properties.size}"
is ElicitRequestURLParams ->
"Creating URL elicitation with message length=${params.message.length}, " +
"elicitationId=${params.elicitationId}"
}
}
logger.trace { "ElicitRequest: $request" }
@ -204,7 +230,19 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client
requestedSchema: ElicitRequestParams.RequestedSchema,
options: RequestOptions?,
): ElicitResult = createElicitation(
request = ElicitRequest(params = ElicitRequestParams(message, requestedSchema)),
request = ElicitRequest(params = ElicitRequestFormParams(message = message, requestedSchema = requestedSchema)),
options = options,
)
override suspend fun createElicitation(
message: String,
elicitationId: String,
url: String,
options: RequestOptions?,
): ElicitResult = createElicitation(
request = ElicitRequest(
params = ElicitRequestURLParams(message = message, elicitationId = elicitationId, url = url),
),
options = options,
)

View file

@ -780,6 +780,30 @@ public open class Server(
options = options,
)
/**
* Triggers URL mode [ClientConnection.createElicitation] request for session by provided [sessionId].
*
* @param sessionId The session ID to create elicitation for.
* @param message The message explaining why the interaction is needed.
* @param elicitationId A unique identifier for the elicitation.
* @param url The URL that the user should navigate to.
* @param options Optional request options.
* @return The created elicitation result.
* @throws IllegalStateException If the server does not support elicitation or if the request fails.
*/
public suspend fun createElicitation(
sessionId: String,
message: String,
elicitationId: String,
url: String,
options: RequestOptions? = null,
): ElicitResult = clientConnection(sessionId).createElicitation(
message = message,
elicitationId = elicitationId,
url = url,
options = options,
)
/**
* Triggers [ClientConnection.sendLoggingMessage] for session by provided [sessionId].
*

View file

@ -382,6 +382,24 @@ public open class ServerSession(
options: RequestOptions? = null,
): ElicitResult = clientConnection.createElicitation(message, requestedSchema, options)
/**
* Sends a URL mode elicitation request to the client, directing the user
* to an external URL for out-of-band interactions.
*
* @param message The message explaining why the interaction is needed.
* @param elicitationId A unique identifier for the elicitation.
* @param url The URL that the user should navigate to.
* @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,
elicitationId: String,
url: String,
options: RequestOptions? = null,
): ElicitResult = clientConnection.createElicitation(message, elicitationId, url, options)
/**
* Sends a logging message notification to the client.
* Messages are filtered based on the current logging level set by the client.