feat!: capabilities for tasks, elicitation (#732)
closes #544 Replaces opaque `JsonObject?` modelling of `elicitation` with a typed `ClientCapabilities.Elicitation` (`form`, `url`), and adds the new `tasks` capability on both `ClientCapabilities` and `ServerCapabilities` (with `list`, `cancel`, per-request-type augmentation). Wires capability assertions for `tasks/*` methods and `notifications/tasks/status` into `Client` and `ServerSession` ## How Has This Been Tested? unit tests ## Breaking Changes yes now elicitation is a specific class in capabilities ## 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:
parent
664f51b9cc
commit
ebc53f0ded
5 changed files with 872 additions and 65 deletions
|
|
@ -38,7 +38,7 @@ import kotlinx.serialization.json.buildJsonObject
|
|||
public class ClientCapabilitiesBuilder @PublishedApi internal constructor() {
|
||||
private var sampling: ClientCapabilities.Sampling? = null
|
||||
private var roots: ClientCapabilities.Roots? = null
|
||||
private var elicitation: JsonObject? = null
|
||||
private var elicitation: ClientCapabilities.Elicitation? = null
|
||||
private var extensions: Map<String, JsonObject>? = null
|
||||
private var experimental: JsonObject? = null
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ public class ClientCapabilitiesBuilder @PublishedApi internal constructor() {
|
|||
*
|
||||
* Pass `ClientCapabilities.Sampling()` to enable base sampling with no sub-capabilities.
|
||||
* Construct `ClientCapabilities.Sampling(tools = EmptyJsonObject, context = EmptyJsonObject)`
|
||||
* directly to enable SEP-1577 sub-capabilities.
|
||||
* directly to enable tools/context sub-capabilities.
|
||||
*
|
||||
* Example:
|
||||
* ```kotlin
|
||||
|
|
@ -99,26 +99,10 @@ public class ClientCapabilitiesBuilder @PublishedApi internal constructor() {
|
|||
*
|
||||
* @param value The elicitation capability configuration
|
||||
*/
|
||||
public fun elicitation(value: JsonObject) {
|
||||
public fun elicitation(value: ClientCapabilities.Elicitation) {
|
||||
this.elicitation = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the client supports elicitation from the server with custom configuration.
|
||||
*
|
||||
* Example:
|
||||
* ```kotlin
|
||||
* capabilities {
|
||||
* elicitation {
|
||||
* put("mode", JsonPrimitive("interactive"))
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param block Lambda for building the elicitation configuration
|
||||
*/
|
||||
public fun elicitation(block: JsonObjectBuilder.() -> Unit): Unit = elicitation(buildJsonObject(block))
|
||||
|
||||
/**
|
||||
* Defines extensions that the client supports.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -41,9 +41,11 @@ public data class Implementation(
|
|||
* supports that capability.
|
||||
*
|
||||
* @property sampling Present if the client supports sampling from an LLM.
|
||||
* Use [ClientCapabilities.Sampling] to configure SEP-1577 sub-capabilities (tools, context).
|
||||
* Use [ClientCapabilities.Sampling] to configure tools/context sub-capabilities.
|
||||
* @property roots Present if the client supports listing roots.
|
||||
* @property elicitation Present if the client supports elicitation from the server.
|
||||
* Use [ClientCapabilities.Elicitation] to configure form/url mode sub-capabilities.
|
||||
* @property tasks Present if the client supports task-augmented requests, listing, or cancellation.
|
||||
* @property experimental Experimental, non-standard capabilities that the client supports.
|
||||
* Keys are capability names, values are capability-specific configuration objects.
|
||||
* @property extensions Optional extensions that the client supports.
|
||||
|
|
@ -54,23 +56,33 @@ public data class Implementation(
|
|||
public data class ClientCapabilities(
|
||||
public val sampling: Sampling? = null,
|
||||
public val roots: Roots? = null,
|
||||
public val elicitation: JsonObject? = null,
|
||||
public val elicitation: Elicitation? = null,
|
||||
public val tasks: Tasks? = null,
|
||||
public val experimental: JsonObject? = null,
|
||||
public val extensions: Map<String, JsonObject>? = null,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Source-compatibility constructor retaining the pre-SEP-1577 `sampling: JsonObject?`
|
||||
* shape. Any non-null [sampling] is converted to an empty [Sampling] (sub-capabilities
|
||||
* cannot be recovered from the old opaque `JsonObject`).
|
||||
* Source-compatibility constructor retaining the older `sampling: JsonObject?` /
|
||||
* `elicitation: JsonObject?` shapes. Any non-null [sampling] is converted to an
|
||||
* empty [Sampling], and any non-null [elicitation] is converted to an empty
|
||||
* [Elicitation] (sub-capabilities cannot be recovered from the old opaque
|
||||
* `JsonObject`).
|
||||
*/
|
||||
@Deprecated(
|
||||
"ClientCapabilities.sampling is now typed. Pass a ClientCapabilities.Sampling? " +
|
||||
"ClientCapabilities.sampling and ClientCapabilities.elicitation are now typed. " +
|
||||
"Pass typed ClientCapabilities.Sampling? / ClientCapabilities.Elicitation? " +
|
||||
"instead of JsonObject?.",
|
||||
ReplaceWith(
|
||||
"ClientCapabilities(sampling?.let { ClientCapabilities.Sampling() }, " +
|
||||
"roots, elicitation, experimental, extensions)",
|
||||
"ClientCapabilities(" +
|
||||
"sampling?.let { ClientCapabilities.Sampling() }, " +
|
||||
"roots, " +
|
||||
"elicitation?.let { ClientCapabilities.Elicitation() }, " +
|
||||
"tasks = null, " +
|
||||
"experimental = experimental, " +
|
||||
"extensions = extensions)",
|
||||
),
|
||||
level = DeprecationLevel.WARNING,
|
||||
)
|
||||
public constructor(
|
||||
sampling: JsonObject?,
|
||||
|
|
@ -81,7 +93,8 @@ public data class ClientCapabilities(
|
|||
) : this(
|
||||
sampling = sampling?.let { Sampling() },
|
||||
roots = roots,
|
||||
elicitation = elicitation,
|
||||
elicitation = elicitation?.let { Elicitation() },
|
||||
tasks = null,
|
||||
experimental = experimental,
|
||||
extensions = extensions,
|
||||
)
|
||||
|
|
@ -101,11 +114,11 @@ public data class ClientCapabilities(
|
|||
|
||||
/**
|
||||
* @property sampling convenience value to enable the base sampling capability with no sub-capabilities
|
||||
* @property elicitation convenience value to enable the elicitation capability
|
||||
* @property elicitation convenience value to enable the elicitation capability with default form mode
|
||||
*/
|
||||
public companion object {
|
||||
public val sampling: Sampling = Sampling()
|
||||
public val elicitation: JsonObject = EmptyJsonObject
|
||||
public val elicitation: Elicitation = Elicitation()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -120,6 +133,60 @@ public data class ClientCapabilities(
|
|||
*/
|
||||
@Serializable
|
||||
public data class Roots(val listChanged: Boolean? = null)
|
||||
|
||||
/**
|
||||
* Sub-capabilities for elicitation.
|
||||
*
|
||||
* An empty [Elicitation] (both fields null) is equivalent to declaring `form` mode only.
|
||||
*
|
||||
* @property form Present if the client supports form-mode elicitation
|
||||
* (in-band structured data collection).
|
||||
* @property url Present if the client supports url-mode elicitation
|
||||
* (out-of-band interaction via URL navigation).
|
||||
*/
|
||||
@Serializable
|
||||
public data class Elicitation(public val form: JsonObject? = null, public val url: JsonObject? = null)
|
||||
|
||||
/**
|
||||
* Sub-capabilities for tasks (client side).
|
||||
*
|
||||
* Declares which client-side requests can be augmented with task execution,
|
||||
* and whether the client supports listing and cancelling tasks.
|
||||
*
|
||||
* @property list Present if the client supports the `tasks/list` operation.
|
||||
* @property cancel Present if the client supports the `tasks/cancel` operation.
|
||||
* @property requests Present if the client supports task-augmented sampling and/or elicitation requests.
|
||||
*/
|
||||
@Serializable
|
||||
public data class Tasks(
|
||||
public val list: JsonObject? = null,
|
||||
public val cancel: JsonObject? = null,
|
||||
public val requests: Requests? = null,
|
||||
) {
|
||||
/**
|
||||
* Task-augmentable client-side request categories.
|
||||
*
|
||||
* @property sampling Present if the client supports task-augmented `sampling/createMessage` requests.
|
||||
* @property elicitation Present if the client supports task-augmented `elicitation/create` requests.
|
||||
*/
|
||||
@Serializable
|
||||
public data class Requests(
|
||||
public val sampling: Sampling? = null,
|
||||
public val elicitation: Elicitation? = null,
|
||||
) {
|
||||
/**
|
||||
* @property createMessage Present if the client supports task-augmented `sampling/createMessage` requests.
|
||||
*/
|
||||
@Serializable
|
||||
public data class Sampling(public val createMessage: JsonObject? = null)
|
||||
|
||||
/**
|
||||
* @property create Present if the client supports task-augmented `elicitation/create` requests.
|
||||
*/
|
||||
@Serializable
|
||||
public data class Elicitation(public val create: JsonObject? = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -137,6 +204,7 @@ public data class ClientCapabilities(
|
|||
* @property logging Present if the server supports sending log messages to the client.
|
||||
* @property completions Present if the server supports argument autocompletion suggestions.
|
||||
* Keys are capability names, values are capability-specific configuration objects.
|
||||
* @property tasks Present if the server supports task-augmented requests, listing, or cancellation.
|
||||
* @property experimental Experimental, non-standard capabilities that the server supports.
|
||||
* @property extensions Optional extensions that the server supports.
|
||||
* Keys are extension identifiers (e.g., `"io.modelcontextprotocol/ui"`),
|
||||
|
|
@ -149,6 +217,7 @@ public data class ServerCapabilities(
|
|||
val prompts: Prompts? = null,
|
||||
val logging: JsonObject? = null,
|
||||
val completions: JsonObject? = null,
|
||||
val tasks: Tasks? = null,
|
||||
val experimental: JsonObject? = null,
|
||||
val extensions: Map<String, JsonObject>? = null,
|
||||
) {
|
||||
|
|
@ -199,4 +268,35 @@ public data class ServerCapabilities(
|
|||
*/
|
||||
@Serializable
|
||||
public data class Prompts(val listChanged: Boolean? = null)
|
||||
|
||||
/**
|
||||
* Sub-capabilities for tasks (server side).
|
||||
*
|
||||
* Declares which server-side requests can be augmented with task execution,
|
||||
* and whether the server supports listing and cancelling tasks.
|
||||
*
|
||||
* @property list Present if the server supports the `tasks/list` operation.
|
||||
* @property cancel Present if the server supports the `tasks/cancel` operation.
|
||||
* @property requests Present if the server supports task-augmented tool call requests.
|
||||
*/
|
||||
@Serializable
|
||||
public data class Tasks(
|
||||
public val list: JsonObject? = null,
|
||||
public val cancel: JsonObject? = null,
|
||||
public val requests: Requests? = null,
|
||||
) {
|
||||
/**
|
||||
* Task-augmentable server-side request categories.
|
||||
*
|
||||
* @property tools Present if the server supports task-augmented `tools/call` requests.
|
||||
*/
|
||||
@Serializable
|
||||
public data class Requests(public val tools: Tools? = null) {
|
||||
/**
|
||||
* @property call Present if the server supports task-augmented `tools/call` requests.
|
||||
*/
|
||||
@Serializable
|
||||
public data class Tools(public val call: JsonObject? = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ class CapabilitiesTest {
|
|||
|
||||
assertEquals(ClientCapabilities.Sampling(), capabilities.sampling)
|
||||
assertEquals(true, capabilities.roots?.listChanged)
|
||||
assertEquals(EmptyJsonObject, capabilities.elicitation)
|
||||
assertEquals(ClientCapabilities.Elicitation(), capabilities.elicitation)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -804,4 +804,123 @@ class CapabilitiesTest {
|
|||
val caps = ClientCapabilities()
|
||||
caps.sampling shouldBe null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should serialize ServerCapabilities with tasks`() {
|
||||
val capabilities = ServerCapabilities(
|
||||
tasks = ServerCapabilities.Tasks(
|
||||
list = EmptyJsonObject,
|
||||
cancel = EmptyJsonObject,
|
||||
requests = ServerCapabilities.Tasks.Requests(
|
||||
tools = ServerCapabilities.Tasks.Requests.Tools(call = EmptyJsonObject),
|
||||
),
|
||||
),
|
||||
)
|
||||
verifySerialization(
|
||||
capabilities,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"tasks": {
|
||||
"list": {},
|
||||
"cancel": {},
|
||||
"requests": {
|
||||
"tools": {
|
||||
"call": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize ServerCapabilities with tasks`() {
|
||||
val json = """
|
||||
{
|
||||
"tasks": {
|
||||
"list": {},
|
||||
"requests": {"tools": {"call": {}}}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val capabilities = verifyDeserialization<ServerCapabilities>(McpJson, json)
|
||||
assertEquals(EmptyJsonObject, capabilities.tasks?.list)
|
||||
assertNull(capabilities.tasks?.cancel)
|
||||
assertEquals(EmptyJsonObject, capabilities.tasks?.requests?.tools?.call)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should serialize ClientCapabilities with tasks`() {
|
||||
val capabilities = ClientCapabilities(
|
||||
tasks = ClientCapabilities.Tasks(
|
||||
list = EmptyJsonObject,
|
||||
cancel = EmptyJsonObject,
|
||||
requests = ClientCapabilities.Tasks.Requests(
|
||||
sampling = ClientCapabilities.Tasks.Requests.Sampling(createMessage = EmptyJsonObject),
|
||||
elicitation = ClientCapabilities.Tasks.Requests.Elicitation(create = EmptyJsonObject),
|
||||
),
|
||||
),
|
||||
)
|
||||
verifySerialization(
|
||||
capabilities,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"tasks": {
|
||||
"list": {},
|
||||
"cancel": {},
|
||||
"requests": {
|
||||
"sampling": {"createMessage": {}},
|
||||
"elicitation": {"create": {}}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should serialize ClientCapabilities with structured elicitation form and url`() {
|
||||
val capabilities = ClientCapabilities(
|
||||
elicitation = ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject),
|
||||
)
|
||||
verifySerialization(
|
||||
capabilities,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"elicitation": {
|
||||
"form": {},
|
||||
"url": {}
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize ClientCapabilities with elicitation and tasks`() {
|
||||
val json = """
|
||||
{
|
||||
"elicitation": {"form": {}, "url": {}},
|
||||
"tasks": {
|
||||
"list": {},
|
||||
"cancel": {},
|
||||
"requests": {
|
||||
"sampling": {"createMessage": {}},
|
||||
"elicitation": {"create": {}}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val capabilities = verifyDeserialization<ClientCapabilities>(McpJson, json)
|
||||
assertEquals(EmptyJsonObject, capabilities.elicitation?.form)
|
||||
assertEquals(EmptyJsonObject, capabilities.elicitation?.url)
|
||||
assertEquals(EmptyJsonObject, capabilities.tasks?.list)
|
||||
assertEquals(EmptyJsonObject, capabilities.tasks?.cancel)
|
||||
assertEquals(EmptyJsonObject, capabilities.tasks?.requests?.sampling?.createMessage)
|
||||
assertEquals(EmptyJsonObject, capabilities.tasks?.requests?.elicitation?.create)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,11 +44,7 @@ class CapabilitiesDslTest {
|
|||
capabilities {
|
||||
sampling(ClientCapabilities.Sampling(tools = EmptyJsonObject))
|
||||
roots(listChanged = true)
|
||||
elicitation {
|
||||
put("mode", "interactive")
|
||||
put("timeout", 30)
|
||||
put("retries", 3)
|
||||
}
|
||||
elicitation(ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject))
|
||||
experimental {
|
||||
put("customFeature", true)
|
||||
put("version", "1.0")
|
||||
|
|
@ -74,9 +70,8 @@ class CapabilitiesDslTest {
|
|||
listChanged shouldBe true
|
||||
}
|
||||
elicitation shouldNotBeNull {
|
||||
get("mode")?.jsonPrimitive?.content shouldBe "interactive"
|
||||
get("timeout")?.jsonPrimitive?.content shouldBe "30"
|
||||
get("retries")?.jsonPrimitive?.content shouldBe "3"
|
||||
form shouldBe EmptyJsonObject
|
||||
url shouldBe EmptyJsonObject
|
||||
}
|
||||
experimental shouldNotBeNull {
|
||||
get("customFeature")?.jsonPrimitive?.content shouldBe "true"
|
||||
|
|
|
|||
|
|
@ -22,9 +22,7 @@ class InitializeDslTest {
|
|||
capabilities {
|
||||
sampling(ClientCapabilities.Sampling())
|
||||
roots(listChanged = true)
|
||||
elicitation {
|
||||
put("mode", "interactive")
|
||||
}
|
||||
elicitation(ClientCapabilities.Elicitation(form = EmptyJsonObject))
|
||||
experimental {
|
||||
put("custom", true)
|
||||
}
|
||||
|
|
@ -46,7 +44,7 @@ class InitializeDslTest {
|
|||
request.params.capabilities.shouldNotBeNull {
|
||||
sampling shouldNotBeNull { }
|
||||
roots?.listChanged shouldBe true
|
||||
elicitation?.get("mode")?.jsonPrimitive?.content shouldBe "interactive"
|
||||
elicitation?.form shouldBe EmptyJsonObject
|
||||
experimental?.get("custom")?.jsonPrimitive?.content shouldBe "true"
|
||||
extensions shouldNotBeNull {
|
||||
get("io.modelcontextprotocol/ui") shouldBe EmptyJsonObject
|
||||
|
|
@ -78,7 +76,7 @@ class InitializeDslTest {
|
|||
@Test
|
||||
fun `capabilities DSL should support direct typed values`() {
|
||||
val samplingValue = ClientCapabilities.Sampling(tools = EmptyJsonObject)
|
||||
val elicitationObj = buildJsonObject { put("key", "value") }
|
||||
val elicitationValue = ClientCapabilities.Elicitation(form = EmptyJsonObject, url = EmptyJsonObject)
|
||||
val experimentalObj = buildJsonObject { put("key", "value") }
|
||||
val extensionsMap = mapOf(
|
||||
"io.modelcontextprotocol/ui" to buildJsonObject { put("key", "value") },
|
||||
|
|
@ -88,7 +86,7 @@ class InitializeDslTest {
|
|||
protocolVersion = "1.0"
|
||||
capabilities {
|
||||
sampling(samplingValue)
|
||||
elicitation(elicitationObj)
|
||||
elicitation(elicitationValue)
|
||||
experimental(experimentalObj)
|
||||
extensions(extensionsMap)
|
||||
}
|
||||
|
|
@ -97,7 +95,7 @@ class InitializeDslTest {
|
|||
|
||||
request.params.capabilities.shouldNotBeNull {
|
||||
sampling shouldBe samplingValue
|
||||
elicitation shouldBe elicitationObj
|
||||
elicitation shouldBe elicitationValue
|
||||
experimental shouldBe experimentalObj
|
||||
extensions shouldBe extensionsMap
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue