Skip to content

Commit fdecf6b

Browse files
authored
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
1 parent ad07232 commit fdecf6b

15 files changed

Lines changed: 1838 additions & 384 deletions

File tree

conformance-test/run-conformance.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ set -uo pipefail
99
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1; pwd)"
1010
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." || exit 1; pwd)"
1111

12-
CONFORMANCE_VERSION="0.1.15"
12+
CONFORMANCE_VERSION="0.1.16"
1313
PORT="${MCP_PORT:-3001}"
1414
SERVER_URL="http://localhost:${PORT}/mcp"
1515
RESULTS_DIR="$SCRIPT_DIR/results"

conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt

Lines changed: 70 additions & 226 deletions
Large diffs are not rendered by default.

integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions
1010
import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
1111
import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest
1212
import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult
13+
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams
1314
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams
1415
import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult
1516
import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject
@@ -35,6 +36,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.Root
3536
import io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification
3637
import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS
3738
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
39+
import io.modelcontextprotocol.kotlin.sdk.types.StringSchema
3840
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
3941
import io.modelcontextprotocol.kotlin.sdk.types.Tool
4042
import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
@@ -48,7 +50,6 @@ import kotlinx.coroutines.test.runTest
4850
import kotlinx.coroutines.withTimeout
4951
import kotlinx.serialization.json.buildJsonObject
5052
import kotlinx.serialization.json.put
51-
import kotlinx.serialization.json.putJsonObject
5253
import kotlin.coroutines.cancellation.CancellationException
5354
import kotlin.test.Test
5455
import kotlin.test.assertEquals
@@ -953,11 +954,7 @@ class ClientTest {
953954
serverSession.createElicitation(
954955
message = "Please provide your GitHub username",
955956
requestedSchema = ElicitRequestParams.RequestedSchema(
956-
properties = buildJsonObject {
957-
putJsonObject("name") {
958-
put("type", "string")
959-
}
960-
},
957+
properties = mapOf("name" to StringSchema()),
961958
required = listOf("name"),
962959
),
963960
)
@@ -1061,11 +1058,7 @@ class ClientTest {
10611058

10621059
val elicitationMessage = "Please provide your GitHub username"
10631060
val requestedSchema = ElicitRequestParams.RequestedSchema(
1064-
properties = buildJsonObject {
1065-
putJsonObject("name") {
1066-
put("type", "string")
1067-
}
1068-
},
1061+
properties = mapOf("name" to StringSchema()),
10691062
required = listOf("name"),
10701063
)
10711064

@@ -1076,7 +1069,8 @@ class ClientTest {
10761069

10771070
client.setElicitationHandler { request ->
10781071
assertEquals(elicitationMessage, request.params.message)
1079-
assertEquals(requestedSchema, request.params.requestedSchema)
1072+
val formParams = request.params as ElicitRequestFormParams
1073+
assertEquals(requestedSchema, formParams.requestedSchema)
10801074

10811075
ElicitResult(
10821076
action = elicitationResultAction,

kotlin-sdk-core/api/kotlin-sdk-core.api

Lines changed: 550 additions & 20 deletions
Large diffs are not rendered by default.

kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.dsl.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
44
import kotlinx.serialization.json.JsonObject
55
import kotlinx.serialization.json.JsonObjectBuilder
66
import kotlinx.serialization.json.buildJsonObject
7+
import kotlinx.serialization.json.decodeFromJsonElement
78
import kotlin.contracts.ExperimentalContracts
89
import kotlin.contracts.InvocationKind
910
import kotlin.contracts.contract
@@ -223,6 +224,9 @@ public class ElicitRequestedSchemaBuilder @PublishedApi internal constructor() {
223224
val properties = requireNotNull(properties) {
224225
"Missing required field 'properties'. Use properties { put(\"fieldName\", schema) }"
225226
}
226-
return ElicitRequestParams.RequestedSchema(properties, required)
227+
val typedProperties: Map<String, PrimitiveSchemaDefinition> = properties.mapValues { (_, value) ->
228+
McpJson.decodeFromJsonElement<PrimitiveSchemaDefinition>(value)
229+
}
230+
return ElicitRequestParams.RequestedSchema(properties = typedProperties, required = required)
227231
}
228232
}

kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/elicitation.kt

Lines changed: 104 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
package io.modelcontextprotocol.kotlin.sdk.types
22

33
import kotlinx.serialization.EncodeDefault
4-
import kotlinx.serialization.ExperimentalSerializationApi
54
import kotlinx.serialization.SerialName
65
import kotlinx.serialization.Serializable
76
import kotlinx.serialization.json.JsonObject
87

98
/**
10-
* A request from the server to elicit additional information from the user via the client.
9+
* Represents an `elicitation/create` request from the server to the client.
1110
*
12-
* This request type allows servers to prompt users for structured input through forms
13-
* or dialogs presented by the client. The server defines a schema for the requested data,
14-
* and the client presents an appropriate UI to collect this information.
11+
* Supports two modes: form mode ([ElicitRequestFormParams]) for collecting structured data
12+
* in-band, and URL mode ([ElicitRequestURLParams]) for directing the user to an external URL.
1513
*
16-
* @property params The elicitation parameters including the message and requested schema.
14+
* @property params The elicitation parameters — either form or URL mode.
1715
*/
1816
@Serializable
1917
public data class ElicitRequest(override val params: ElicitRequestParams) : ServerRequest {
20-
@OptIn(ExperimentalSerializationApi::class)
2118
@EncodeDefault
2219
public override val method: Method = Method.Defined.ElicitationCreate
2320

@@ -30,8 +27,13 @@ public data class ElicitRequest(override val params: ElicitRequestParams) : Serv
3027
/**
3128
* A restricted subset of JSON Schema defining the structure of the requested data.
3229
*/
33-
public val requestedSchema: ElicitRequestParams.RequestedSchema
34-
get() = params.requestedSchema
30+
@Deprecated(
31+
"Use (params as ElicitRequestFormParams).requestedSchema",
32+
ReplaceWith("(params as ElicitRequestFormParams).requestedSchema"),
33+
DeprecationLevel.WARNING,
34+
)
35+
public val requestedSchema: ElicitRequestParams.RequestedSchema?
36+
get() = (params as? ElicitRequestFormParams)?.requestedSchema
3537

3638
/**
3739
* Metadata for this request. May include a progressToken for out-of-band progress notifications.
@@ -41,55 +43,124 @@ public data class ElicitRequest(override val params: ElicitRequestParams) : Serv
4143
}
4244

4345
/**
44-
* Parameters for an elicitation/create request.
46+
* Represents the parameters for an `elicitation/create` request.
4547
*
46-
* @property message The message to present to the user. This should clearly explain
47-
* what information is being requested and why.
48-
* @property requestedSchema A restricted subset of JSON Schema defining the structure
49-
* of the requested data. Only top-level properties are allowed,
50-
* without nesting.
51-
* @property meta Optional metadata for this request. May include a progressToken for
52-
* out-of-band progress notifications.
48+
* Implementations: [ElicitRequestFormParams], [ElicitRequestURLParams].
5349
*/
54-
@Serializable
55-
public data class ElicitRequestParams(
56-
val message: String,
57-
val requestedSchema: RequestedSchema,
58-
@SerialName("_meta")
59-
override val meta: RequestMeta? = null,
60-
) : RequestParams {
50+
@Serializable(with = ElicitRequestParamsSerializer::class)
51+
public sealed interface ElicitRequestParams : RequestParams {
52+
public val message: String
6153

6254
/**
6355
* A restricted JSON Schema for elicitation requests.
6456
*
6557
* Only supports top-level primitive properties without nesting. Each property
6658
* represents a field in the form or dialog presented to the user.
6759
*
60+
* @property schema Optional URI to a JSON Schema definition.
6861
* @property properties A map of property names to their schema definitions.
6962
* Each property must be a primitive type (string, number, boolean).
7063
* @property required Optional list of property names that must be provided by the user.
7164
* If omitted, all fields are considered optional.
7265
* @property type Always "object" for elicitation schemas.
7366
*/
7467
@Serializable
75-
public data class RequestedSchema(val properties: JsonObject, val required: List<String>? = null) {
76-
@OptIn(ExperimentalSerializationApi::class)
68+
public data class RequestedSchema(
69+
@SerialName($$"$schema")
70+
val schema: String? = null,
71+
val properties: Map<String, PrimitiveSchemaDefinition>,
72+
val required: List<String>? = null,
73+
) {
7774
@EncodeDefault
7875
val type: String = "object"
7976
}
8077
}
8178

8279
/**
83-
* The client's response to an [ElicitRequest].
80+
* Creates an [ElicitRequestFormParams] for backwards compatibility.
81+
*
82+
* @param message The message to present to the user.
83+
* @param requestedSchema The JSON Schema for the requested data.
84+
* @param meta Optional request metadata.
85+
* @return A configured [ElicitRequestFormParams] instance.
86+
*/
87+
@Deprecated(
88+
"Use ElicitRequestFormParams instead",
89+
ReplaceWith("ElicitRequestFormParams(message, requestedSchema = requestedSchema, meta = meta)"),
90+
DeprecationLevel.WARNING,
91+
)
92+
@Suppress("FunctionNaming", "FunctionName")
93+
public fun ElicitRequestParams(
94+
message: String,
95+
requestedSchema: ElicitRequestParams.RequestedSchema,
96+
meta: RequestMeta? = null,
97+
): ElicitRequestFormParams = ElicitRequestFormParams(
98+
message = message,
99+
requestedSchema = requestedSchema,
100+
meta = meta,
101+
)
102+
103+
/**
104+
* Represents form mode parameters for an `elicitation/create` request.
105+
*
106+
* Collects non-sensitive structured data from the user via a form presented by the client.
84107
*
85-
* Represents the user's action and, if accepted, the submitted form data.
108+
* @property message The message to present to the user describing what information is being requested.
109+
* @property task If specified, the caller is requesting task-augmented execution. The request
110+
* will return a [CreateTaskResult] immediately, and the actual result can be retrieved
111+
* later via `tasks/result`.
112+
* @property requestedSchema A restricted subset of JSON Schema. Only top-level properties
113+
* are allowed, without nesting.
114+
* @property meta Optional metadata. May include a progressToken for out-of-band progress notifications.
115+
*/
116+
@Serializable
117+
public data class ElicitRequestFormParams(
118+
override val message: String,
119+
val task: TaskMetadata? = null,
120+
val requestedSchema: ElicitRequestParams.RequestedSchema,
121+
@SerialName("_meta")
122+
override val meta: RequestMeta? = null,
123+
) : ElicitRequestParams {
124+
@EncodeDefault
125+
public val mode: String = "form"
126+
}
127+
128+
/**
129+
* Represents URL mode parameters for an `elicitation/create` request.
130+
*
131+
* Directs the user to an external URL for out-of-band interactions (e.g., OAuth flows,
132+
* payment processing, or entering sensitive credentials) that must not pass through the MCP client.
133+
*
134+
* @property message The message explaining why the interaction is needed.
135+
* @property elicitationId A unique identifier for this elicitation. The client MUST treat
136+
* this ID as an opaque value.
137+
* @property url The URL that the user should navigate to.
138+
* @property task If specified, the caller is requesting task-augmented execution. The request
139+
* will return a [CreateTaskResult] immediately, and the actual result can be retrieved
140+
* later via `tasks/result`.
141+
* @property meta Optional metadata. May include a progressToken for out-of-band progress notifications.
142+
*/
143+
@Serializable
144+
public data class ElicitRequestURLParams(
145+
override val message: String,
146+
val elicitationId: String,
147+
val url: String,
148+
val task: TaskMetadata? = null,
149+
@SerialName("_meta")
150+
override val meta: RequestMeta? = null,
151+
) : ElicitRequestParams {
152+
@EncodeDefault
153+
public val mode: String = "url"
154+
}
155+
156+
/**
157+
* Represents the client's response to an [ElicitRequest].
86158
*
87-
* @property action The user action in response to the elicitation prompt.
88-
* @property content The submitted form data, only present when [action] is [Action.Accept].
89-
* Contains values matching the requested schema, where keys correspond
90-
* to property names and values are primitives (string, number, or boolean).
159+
* @property action The user action in response to the elicitation.
160+
* @property content The submitted form data, only present when [action] is [Action.Accept]
161+
* and mode was form. Contains values matching the requested schema. Omitted for
162+
* URL mode responses.
91163
* @property meta Optional metadata for this response.
92-
* @throws IllegalArgumentException if content is provided with a non-accept action.
93164
*/
94165
@Serializable
95166
public data class ElicitResult(

0 commit comments

Comments
 (0)