feat: add tasks models (#566)
## Motivation and Context closes #421 #406 ## How Has This Been Tested? Unit tests ## Breaking Changes NaN ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] 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
62e4e50c67
commit
fd3a8583b8
7 changed files with 1904 additions and 0 deletions
|
|
@ -32,6 +32,7 @@ public sealed interface Method {
|
|||
NotificationsToolsListChanged("notifications/tools/list_changed"),
|
||||
NotificationsRootsListChanged("notifications/roots/list_changed"),
|
||||
NotificationsPromptsListChanged("notifications/prompts/list_changed"),
|
||||
NotificationsTasksStatus("notifications/tasks/status"),
|
||||
ToolsList("tools/list"),
|
||||
ToolsCall("tools/call"),
|
||||
LoggingSetLevel("logging/setLevel"),
|
||||
|
|
@ -39,6 +40,10 @@ public sealed interface Method {
|
|||
CompletionComplete("completion/complete"),
|
||||
RootsList("roots/list"),
|
||||
ElicitationCreate("elicitation/create"),
|
||||
TasksGet("tasks/get"),
|
||||
TasksResult("tasks/result"),
|
||||
TasksList("tasks/list"),
|
||||
TasksCancel("tasks/cancel"),
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -348,3 +348,48 @@ public data class ToolListChangedNotification(override val params: BaseNotificat
|
|||
@EncodeDefault
|
||||
override val method: Method = Method.Defined.NotificationsToolsListChanged
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Task Status Notification
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* An optional notification from the receiver to the requestor, informing them that a task’s status has changed.
|
||||
* Receivers are not required to send these notifications.
|
||||
*
|
||||
* This notification can be sent by either side (both [ClientNotification] and [ServerNotification]).
|
||||
*
|
||||
* @property params The task status notification parameters containing the current task state.
|
||||
*/
|
||||
@Serializable
|
||||
public data class TaskStatusNotification(override val params: TaskStatusNotificationParams? = null) :
|
||||
ClientNotification,
|
||||
ServerNotification {
|
||||
@EncodeDefault
|
||||
override val method: Method = Method.Defined.NotificationsTasksStatus
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for a notifications/tasks/status notification.
|
||||
*
|
||||
* @property taskId The task identifier.
|
||||
* @property status Current task state.
|
||||
* @property statusMessage Optional human-readable message describing the current task state.
|
||||
* @property createdAt ISO 8601 timestamp when the task was created.
|
||||
* @property lastUpdatedAt ISO 8601 timestamp when the task was last updated.
|
||||
* @property ttl Actual retention duration from creation in milliseconds, null for unlimited.
|
||||
* @property pollInterval Suggested polling interval in milliseconds.
|
||||
* @property meta Optional metadata for this notification.
|
||||
*/
|
||||
@Serializable
|
||||
public data class TaskStatusNotificationParams(
|
||||
override val taskId: String,
|
||||
override val status: TaskStatus,
|
||||
override val statusMessage: String? = null,
|
||||
override val createdAt: String,
|
||||
override val lastUpdatedAt: String,
|
||||
override val ttl: Long?,
|
||||
override val pollInterval: Long? = null,
|
||||
@SerialName("_meta") override val meta: JsonObject? = null,
|
||||
) : NotificationParams,
|
||||
TaskFields
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable
|
|||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import kotlinx.serialization.json.long
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
import kotlin.jvm.JvmInline
|
||||
|
|
@ -22,6 +23,17 @@ public value class RequestMeta(public val json: JsonObject) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The related task metadata, if this request is associated with a task.
|
||||
*
|
||||
* @see RelatedTaskMetadata
|
||||
* @see RELATED_TASK_META_KEY
|
||||
*/
|
||||
public val relatedTask: RelatedTaskMetadata?
|
||||
get() = json[RELATED_TASK_META_KEY]?.let { element ->
|
||||
McpJson.decodeFromJsonElement(element)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value associated with the specified key from the JSON object.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -159,6 +159,10 @@ private val clientRequestDeserializers: Map<String, DeserializationStrategy<Clie
|
|||
Method.Defined.ResourcesTemplatesList.value to ListResourceTemplatesRequest.serializer(),
|
||||
Method.Defined.ToolsCall.value to CallToolRequest.serializer(),
|
||||
Method.Defined.ToolsList.value to ListToolsRequest.serializer(),
|
||||
Method.Defined.TasksGet.value to GetTaskRequest.serializer(),
|
||||
Method.Defined.TasksResult.value to GetTaskPayloadRequest.serializer(),
|
||||
Method.Defined.TasksList.value to ListTasksRequest.serializer(),
|
||||
Method.Defined.TasksCancel.value to CancelTaskRequest.serializer(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -175,6 +179,10 @@ private val serverRequestDeserializers: Map<String, DeserializationStrategy<Serv
|
|||
Method.Defined.Ping.value to PingRequest.serializer(),
|
||||
Method.Defined.RootsList.value to ListRootsRequest.serializer(),
|
||||
Method.Defined.SamplingCreateMessage.value to CreateMessageRequest.serializer(),
|
||||
Method.Defined.TasksGet.value to GetTaskRequest.serializer(),
|
||||
Method.Defined.TasksResult.value to GetTaskPayloadRequest.serializer(),
|
||||
Method.Defined.TasksList.value to ListTasksRequest.serializer(),
|
||||
Method.Defined.TasksCancel.value to CancelTaskRequest.serializer(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -212,6 +220,7 @@ private val clientNotificationDeserializers: Map<String, DeserializationStrategy
|
|||
Method.Defined.NotificationsProgress.value to ProgressNotification.serializer(),
|
||||
Method.Defined.NotificationsInitialized.value to InitializedNotification.serializer(),
|
||||
Method.Defined.NotificationsRootsListChanged.value to RootsListChangedNotification.serializer(),
|
||||
Method.Defined.NotificationsTasksStatus.value to TaskStatusNotification.serializer(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -231,6 +240,7 @@ private val serverNotificationDeserializers: Map<String, DeserializationStrategy
|
|||
Method.Defined.NotificationsResourcesListChanged.value to ResourceListChangedNotification.serializer(),
|
||||
Method.Defined.NotificationsToolsListChanged.value to ToolListChangedNotification.serializer(),
|
||||
Method.Defined.NotificationsPromptsListChanged.value to PromptListChangedNotification.serializer(),
|
||||
Method.Defined.NotificationsTasksStatus.value to TaskStatusNotification.serializer(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -301,6 +311,9 @@ private fun selectClientResultDeserializer(element: JsonElement): Deserializatio
|
|||
"model" in jsonObject && "role" in jsonObject -> CreateMessageResult.serializer()
|
||||
"roots" in jsonObject -> ListRootsResult.serializer()
|
||||
"action" in jsonObject -> ElicitResult.serializer()
|
||||
"task" in jsonObject -> CreateTaskResult.serializer()
|
||||
"tasks" in jsonObject -> ListTasksResult.serializer()
|
||||
"taskId" in jsonObject -> GetTaskResult.serializer()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -309,6 +322,7 @@ private fun selectClientResultDeserializer(element: JsonElement): Deserializatio
|
|||
* Selects the appropriate deserializer for server results based on JSON content.
|
||||
* Returns null if the structure doesn't match any known server result type.
|
||||
*/
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun selectServerResultDeserializer(element: JsonElement): DeserializationStrategy<ServerResult>? {
|
||||
val jsonObject = element.jsonObject
|
||||
return when {
|
||||
|
|
@ -321,6 +335,9 @@ private fun selectServerResultDeserializer(element: JsonElement): Deserializatio
|
|||
"messages" in jsonObject -> GetPromptResult.serializer()
|
||||
"contents" in jsonObject -> ReadResourceResult.serializer()
|
||||
"content" in jsonObject -> CallToolResult.serializer()
|
||||
"task" in jsonObject -> CreateTaskResult.serializer()
|
||||
"tasks" in jsonObject -> ListTasksResult.serializer()
|
||||
"taskId" in jsonObject -> GetTaskResult.serializer()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,347 @@
|
|||
@file:OptIn(ExperimentalSerializationApi::class)
|
||||
|
||||
package io.modelcontextprotocol.kotlin.sdk.types
|
||||
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
/**
|
||||
* The key used in `_meta` to associate a message with a task.
|
||||
*/
|
||||
public const val RELATED_TASK_META_KEY: String = "io.modelcontextprotocol/related-task"
|
||||
|
||||
/**
|
||||
* Common fields shared by all types representing the task state.
|
||||
*/
|
||||
public sealed interface TaskFields {
|
||||
public val taskId: String
|
||||
public val status: TaskStatus
|
||||
public val statusMessage: String?
|
||||
public val createdAt: String
|
||||
public val lastUpdatedAt: String
|
||||
public val ttl: Long?
|
||||
public val pollInterval: Long?
|
||||
}
|
||||
|
||||
/**
|
||||
* Data associated with a task.
|
||||
*
|
||||
* @property taskId The task identifier.
|
||||
* @property status Current task state.
|
||||
* @property statusMessage Optional human-readable message describing the current task state.
|
||||
* This can provide context for any status, including reasons for "cancelled" status,
|
||||
* summaries for "completed" status, or diagnostic information for "failed" status.
|
||||
* @property createdAt ISO 8601 timestamp when the task was created.
|
||||
* @property lastUpdatedAt ISO 8601 timestamp when the task was last updated.
|
||||
* @property ttl Actual retention duration from creation in milliseconds, null for unlimited.
|
||||
* @property pollInterval Suggested polling interval in milliseconds.
|
||||
*/
|
||||
@Serializable
|
||||
public data class Task(
|
||||
override val taskId: String,
|
||||
override val status: TaskStatus,
|
||||
override val statusMessage: String? = null,
|
||||
override val createdAt: String,
|
||||
override val lastUpdatedAt: String,
|
||||
override val ttl: Long?,
|
||||
override val pollInterval: Long? = null,
|
||||
) : TaskFields
|
||||
|
||||
/**
|
||||
* Metadata for augmenting a request with task execution.
|
||||
* Include this in the `task` field of the request parameters.
|
||||
*
|
||||
* @property ttl Requested duration in milliseconds to retain task from creation.
|
||||
*/
|
||||
@Serializable
|
||||
public data class TaskMetadata(val ttl: Long? = null)
|
||||
|
||||
/**
|
||||
* The status of a task.
|
||||
*/
|
||||
@Serializable
|
||||
public enum class TaskStatus {
|
||||
@SerialName("working")
|
||||
Working,
|
||||
|
||||
@SerialName("input_required")
|
||||
InputRequired,
|
||||
|
||||
@SerialName("completed")
|
||||
Completed,
|
||||
|
||||
@SerialName("failed")
|
||||
Failed,
|
||||
|
||||
@SerialName("cancelled")
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for associating messages with a task.
|
||||
* Include this in the `_meta` field under the key `io.modelcontextprotocol/related-task`.
|
||||
*
|
||||
* @property taskId The task identifier this message is associated with.
|
||||
*/
|
||||
@Serializable
|
||||
public data class RelatedTaskMetadata(val taskId: String)
|
||||
|
||||
/**
|
||||
* A response to a task-augmented request.
|
||||
*
|
||||
* @property task The task data associated with the response.
|
||||
* @property meta Optional metadata for this response.
|
||||
*/
|
||||
@Serializable
|
||||
public data class CreateTaskResult(
|
||||
val task: Task,
|
||||
@SerialName("_meta")
|
||||
override val meta: JsonObject? = null,
|
||||
) : ClientResult,
|
||||
ServerResult
|
||||
|
||||
// ============================================================================
|
||||
// tasks/get
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* A request to retrieve the state of a task.
|
||||
*
|
||||
* This request can be sent by either side (both [ClientRequest] and [ServerRequest]).
|
||||
*
|
||||
* @property params The parameters specifying which task to retrieve.
|
||||
*/
|
||||
@Serializable
|
||||
public data class GetTaskRequest(override val params: GetTaskRequestParams) :
|
||||
ClientRequest,
|
||||
ServerRequest {
|
||||
@EncodeDefault
|
||||
override val method: Method = Method.Defined.TasksGet
|
||||
|
||||
/**
|
||||
* The task identifier to query.
|
||||
*/
|
||||
public val taskId: String
|
||||
get() = params.taskId
|
||||
|
||||
/**
|
||||
* Metadata for this request. May include a progressToken for out-of-band progress notifications.
|
||||
*/
|
||||
public val meta: RequestMeta?
|
||||
get() = params.meta
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for a tasks/get request.
|
||||
*
|
||||
* @property taskId The task identifier to query.
|
||||
* @property meta Optional metadata for this request.
|
||||
*/
|
||||
@Serializable
|
||||
public data class GetTaskRequestParams(
|
||||
val taskId: String,
|
||||
@SerialName("_meta")
|
||||
override val meta: RequestMeta? = null,
|
||||
) : RequestParams
|
||||
|
||||
/**
|
||||
* The response to a tasks/get request.
|
||||
*
|
||||
* Contains all [Task] fields flattened into the result (intersection of Result and Task).
|
||||
*
|
||||
* @property taskId The task identifier.
|
||||
* @property status Current task state.
|
||||
* @property statusMessage Optional human-readable message describing the current task state.
|
||||
* @property createdAt ISO 8601 timestamp when the task was created.
|
||||
* @property lastUpdatedAt ISO 8601 timestamp when the task was last updated.
|
||||
* @property ttl Actual retention duration from creation in milliseconds, null for unlimited.
|
||||
* @property pollInterval Suggested polling interval in milliseconds.
|
||||
* @property meta Optional metadata for this response.
|
||||
*/
|
||||
@Serializable
|
||||
public data class GetTaskResult(
|
||||
override val taskId: String,
|
||||
override val status: TaskStatus,
|
||||
override val statusMessage: String? = null,
|
||||
override val createdAt: String,
|
||||
override val lastUpdatedAt: String,
|
||||
override val ttl: Long?,
|
||||
override val pollInterval: Long? = null,
|
||||
@SerialName("_meta")
|
||||
override val meta: JsonObject? = null,
|
||||
) : ClientResult,
|
||||
ServerResult,
|
||||
TaskFields
|
||||
|
||||
// ============================================================================
|
||||
// tasks/result
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* A request to retrieve the result of a completed task.
|
||||
*
|
||||
* This request can be sent by either side (both [ClientRequest] and [ServerRequest]).
|
||||
*
|
||||
* @property params The parameters specifying which task to retrieve results for.
|
||||
*/
|
||||
@Serializable
|
||||
public data class GetTaskPayloadRequest(override val params: GetTaskPayloadRequestParams) :
|
||||
ClientRequest,
|
||||
ServerRequest {
|
||||
@EncodeDefault
|
||||
override val method: Method = Method.Defined.TasksResult
|
||||
|
||||
/**
|
||||
* The task identifier to retrieve results for.
|
||||
*/
|
||||
public val taskId: String
|
||||
get() = params.taskId
|
||||
|
||||
/**
|
||||
* Metadata for this request. May include a progressToken for out-of-band progress notifications.
|
||||
*/
|
||||
public val meta: RequestMeta?
|
||||
get() = params.meta
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for a tasks/result request.
|
||||
*
|
||||
* @property taskId The task identifier to retrieve results for.
|
||||
* @property meta Optional metadata for this request.
|
||||
*/
|
||||
@Serializable
|
||||
public data class GetTaskPayloadRequestParams(
|
||||
val taskId: String,
|
||||
@SerialName("_meta")
|
||||
override val meta: RequestMeta? = null,
|
||||
) : RequestParams
|
||||
|
||||
/**
|
||||
* The response to a tasks/result request.
|
||||
*
|
||||
* The structure matches the result type of the original request.
|
||||
* For example, a tools/call task would return the [CallToolResult] structure.
|
||||
*
|
||||
* @property meta Optional metadata for this response.
|
||||
*/
|
||||
@JvmInline
|
||||
@Serializable
|
||||
public value class GetTaskPayloadResult(public val json: JsonObject) :
|
||||
ClientResult,
|
||||
ServerResult {
|
||||
override val meta: JsonObject?
|
||||
get() = json["_meta"]?.takeIf { it is JsonObject } as? JsonObject
|
||||
|
||||
/**
|
||||
* Retrieves the value associated with the specified key from the result payload.
|
||||
*
|
||||
* @param key the key whose corresponding value is to be returned
|
||||
* @return the [JsonElement] associated with the specified key, or null if the key does not exist
|
||||
*/
|
||||
public operator fun get(key: String): JsonElement? = json[key]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// tasks/list
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* A request to retrieve a list of tasks.
|
||||
*
|
||||
* This request can be sent by either side (both [ClientRequest] and [ServerRequest]).
|
||||
* Supports pagination through the cursor parameter.
|
||||
*
|
||||
* @property params Optional pagination parameters to control which page of results to return.
|
||||
*/
|
||||
@Serializable
|
||||
public data class ListTasksRequest(override val params: PaginatedRequestParams? = null) :
|
||||
ClientRequest,
|
||||
ServerRequest,
|
||||
PaginatedRequest {
|
||||
@EncodeDefault
|
||||
override val method: Method = Method.Defined.TasksList
|
||||
}
|
||||
|
||||
/**
|
||||
* The response to a tasks/list request.
|
||||
*
|
||||
* @property tasks The list of tasks.
|
||||
* @property nextCursor An opaque token representing the pagination position after the last returned result.
|
||||
* If present, there may be more results available.
|
||||
* @property meta Optional metadata for this response.
|
||||
*/
|
||||
@Serializable
|
||||
public data class ListTasksResult(
|
||||
val tasks: List<Task>,
|
||||
override val nextCursor: String? = null,
|
||||
@SerialName("_meta")
|
||||
override val meta: JsonObject? = null,
|
||||
) : ClientResult,
|
||||
ServerResult,
|
||||
PaginatedResult
|
||||
|
||||
// ============================================================================
|
||||
// tasks/cancel
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* A request to cancel a task.
|
||||
*
|
||||
* This request can be sent by either side (both [ClientRequest] and [ServerRequest]).
|
||||
*
|
||||
* @property params The parameters specifying which task to cancel.
|
||||
*/
|
||||
@Serializable
|
||||
public data class CancelTaskRequest(override val params: CancelTaskRequestParams) :
|
||||
ClientRequest,
|
||||
ServerRequest {
|
||||
@EncodeDefault
|
||||
override val method: Method = Method.Defined.TasksCancel
|
||||
|
||||
/**
|
||||
* The task identifier to cancel.
|
||||
*/
|
||||
public val taskId: String
|
||||
get() = params.taskId
|
||||
|
||||
/**
|
||||
* Metadata for this request. May include a progressToken for out-of-band progress notifications.
|
||||
*/
|
||||
public val meta: RequestMeta?
|
||||
get() = params.meta
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for a tasks/cancel request.
|
||||
*
|
||||
* @property taskId The task identifier to cancel.
|
||||
* @property meta Optional metadata for this request.
|
||||
*/
|
||||
@Serializable
|
||||
public data class CancelTaskRequestParams(
|
||||
val taskId: String,
|
||||
@SerialName("_meta")
|
||||
override val meta: RequestMeta? = null,
|
||||
) : RequestParams
|
||||
|
||||
/**
|
||||
* The response to a tasks/cancel request.
|
||||
*
|
||||
* Contains all [Task] fields flattened into the result (intersection of Result and Task).
|
||||
*
|
||||
* @property taskId The task identifier.
|
||||
* @property status Current task state.
|
||||
* @property statusMessage Optional human-readable message describing the current task state.
|
||||
* @property createdAt ISO 8601 timestamp when the task was created.
|
||||
* @property lastUpdatedAt ISO 8601 timestamp when the task was last updated.
|
||||
* @property ttl Actual retention duration from creation in milliseconds, null for unlimited.
|
||||
* @property pollInterval Suggested polling interval in milliseconds.
|
||||
* @property meta Optional metadata for this response.
|
||||
*/
|
||||
public typealias CancelTaskResult = GetTaskResult
|
||||
|
|
@ -310,4 +310,116 @@ class NotificationTest {
|
|||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should serialize TaskStatusNotification with all fields`() {
|
||||
val notification = TaskStatusNotification(
|
||||
TaskStatusNotificationParams(
|
||||
taskId = "task-1",
|
||||
status = TaskStatus.Working,
|
||||
statusMessage = "Processing data",
|
||||
createdAt = "2025-01-01T00:00:00Z",
|
||||
lastUpdatedAt = "2025-01-01T00:01:00Z",
|
||||
ttl = 60000,
|
||||
pollInterval = 5000,
|
||||
meta = buildJsonObject { put("source", "worker-1") },
|
||||
),
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
notification,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"method": "notifications/tasks/status",
|
||||
"params": {
|
||||
"taskId": "task-1",
|
||||
"status": "working",
|
||||
"statusMessage": "Processing data",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:01:00Z",
|
||||
"ttl": 60000,
|
||||
"pollInterval": 5000,
|
||||
"_meta": {
|
||||
"source": "worker-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should serialize TaskStatusNotification with minimal fields`() {
|
||||
val notification = TaskStatusNotification(
|
||||
TaskStatusNotificationParams(
|
||||
taskId = "task-2",
|
||||
status = TaskStatus.Completed,
|
||||
createdAt = "2025-01-01T00:00:00Z",
|
||||
lastUpdatedAt = "2025-01-01T00:02:00Z",
|
||||
ttl = null,
|
||||
),
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
notification,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"method": "notifications/tasks/status",
|
||||
"params": {
|
||||
"taskId": "task-2",
|
||||
"status": "completed",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:02:00Z"
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize TaskStatusNotification`() {
|
||||
val json = """
|
||||
{
|
||||
"method": "notifications/tasks/status",
|
||||
"params": {
|
||||
"taskId": "task-3",
|
||||
"status": "failed",
|
||||
"statusMessage": "Connection lost",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:03:00Z",
|
||||
"ttl": 30000,
|
||||
"pollInterval": 1000
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val notification = verifyDeserialization<TaskStatusNotification>(McpJson, json)
|
||||
val params = notification.params!!
|
||||
|
||||
assertEquals(Method.Defined.NotificationsTasksStatus, notification.method)
|
||||
assertEquals("task-3", params.taskId)
|
||||
assertEquals(TaskStatus.Failed, params.status)
|
||||
assertEquals("Connection lost", params.statusMessage)
|
||||
assertEquals("2025-01-01T00:00:00Z", params.createdAt)
|
||||
assertEquals("2025-01-01T00:03:00Z", params.lastUpdatedAt)
|
||||
assertEquals(30000L, params.ttl)
|
||||
assertEquals(1000L, params.pollInterval)
|
||||
assertNull(params.meta)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize TaskStatusNotification without params`() {
|
||||
val json = """
|
||||
{
|
||||
"method": "notifications/tasks/status"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val notification = verifyDeserialization<TaskStatusNotification>(McpJson, json)
|
||||
|
||||
assertEquals(Method.Defined.NotificationsTasksStatus, notification.method)
|
||||
assertNull(notification.params)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,802 @@
|
|||
package io.modelcontextprotocol.kotlin.sdk.types
|
||||
|
||||
import io.kotest.matchers.nulls.shouldBeNull
|
||||
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.types.shouldBeInstanceOf
|
||||
import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization
|
||||
import io.modelcontextprotocol.kotlin.test.utils.verifySerialization
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlin.test.Test
|
||||
|
||||
class TasksTest {
|
||||
|
||||
// ========================================================================
|
||||
// Task
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize Task with minimal fields`() {
|
||||
val task = Task(
|
||||
taskId = "task-1",
|
||||
status = TaskStatus.Working,
|
||||
createdAt = "2025-01-01T00:00:00Z",
|
||||
lastUpdatedAt = "2025-01-01T00:00:00Z",
|
||||
ttl = null,
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
task,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"taskId": "task-1",
|
||||
"status": "working",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:00:00Z"
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should serialize Task with all fields`() {
|
||||
val task = Task(
|
||||
taskId = "task-2",
|
||||
status = TaskStatus.Completed,
|
||||
statusMessage = "Processing complete",
|
||||
createdAt = "2025-01-01T00:00:00Z",
|
||||
lastUpdatedAt = "2025-01-01T00:01:00Z",
|
||||
ttl = 60000,
|
||||
pollInterval = 5000,
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
task,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"taskId": "task-2",
|
||||
"status": "completed",
|
||||
"statusMessage": "Processing complete",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:01:00Z",
|
||||
"ttl": 60000,
|
||||
"pollInterval": 5000
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize Task and verify TaskFields contract`() {
|
||||
val json = """
|
||||
{
|
||||
"taskId": "task-3",
|
||||
"status": "failed",
|
||||
"statusMessage": "Connection lost",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:02:00Z",
|
||||
"ttl": 30000,
|
||||
"pollInterval": 1000
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val task = verifyDeserialization<Task>(McpJson, json)
|
||||
task.shouldBeInstanceOf<TaskFields>()
|
||||
task.taskId shouldBe "task-3"
|
||||
task.status shouldBe TaskStatus.Failed
|
||||
task.statusMessage shouldBe "Connection lost"
|
||||
task.createdAt shouldBe "2025-01-01T00:00:00Z"
|
||||
task.lastUpdatedAt shouldBe "2025-01-01T00:02:00Z"
|
||||
task.ttl shouldBe 30000L
|
||||
task.pollInterval shouldBe 1000L
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TaskStatus
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize all TaskStatus values`() {
|
||||
verifySerialization(TaskStatus.Working, McpJson, "\"working\"")
|
||||
verifySerialization(TaskStatus.InputRequired, McpJson, "\"input_required\"")
|
||||
verifySerialization(TaskStatus.Completed, McpJson, "\"completed\"")
|
||||
verifySerialization(TaskStatus.Failed, McpJson, "\"failed\"")
|
||||
verifySerialization(TaskStatus.Cancelled, McpJson, "\"cancelled\"")
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TaskMetadata
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize TaskMetadata with ttl`() {
|
||||
val metadata = TaskMetadata(ttl = 120000)
|
||||
|
||||
verifySerialization(
|
||||
metadata,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"ttl": 120000
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should serialize TaskMetadata without ttl`() {
|
||||
val metadata = TaskMetadata()
|
||||
|
||||
verifySerialization(
|
||||
metadata,
|
||||
McpJson,
|
||||
"{}",
|
||||
)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// RelatedTaskMetadata
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize RelatedTaskMetadata`() {
|
||||
val related = RelatedTaskMetadata(taskId = "task-42")
|
||||
|
||||
verifySerialization(
|
||||
related,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"taskId": "task-42"
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize RequestMeta with RelatedTaskMetadata`() {
|
||||
val json = """
|
||||
{
|
||||
"io.modelcontextprotocol/related-task": {
|
||||
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val meta = McpJson.decodeFromString<RequestMeta>(json)
|
||||
val related = meta.relatedTask
|
||||
related.shouldNotBeNull()
|
||||
related.taskId shouldBe "786512e2-9e0d-44bd-8f29-789f320fe840"
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// CreateTaskResult
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize CreateTaskResult`() {
|
||||
val result = CreateTaskResult(
|
||||
task = Task(
|
||||
taskId = "task-1",
|
||||
status = TaskStatus.Working,
|
||||
createdAt = "2025-01-01T00:00:00Z",
|
||||
lastUpdatedAt = "2025-01-01T00:00:00Z",
|
||||
ttl = null,
|
||||
),
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
result,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"task": {
|
||||
"taskId": "task-1",
|
||||
"status": "working",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should serialize CreateTaskResult with meta`() {
|
||||
val result = CreateTaskResult(
|
||||
task = Task(
|
||||
taskId = "task-1",
|
||||
status = TaskStatus.Working,
|
||||
createdAt = "2025-01-01T00:00:00Z",
|
||||
lastUpdatedAt = "2025-01-01T00:00:00Z",
|
||||
ttl = 60000,
|
||||
),
|
||||
meta = buildJsonObject { put("trace", "abc") },
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
result,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"task": {
|
||||
"taskId": "task-1",
|
||||
"status": "working",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:00:00Z",
|
||||
"ttl": 60000
|
||||
},
|
||||
"_meta": {
|
||||
"trace": "abc"
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize CreateTaskResult`() {
|
||||
val json = """
|
||||
{
|
||||
"task": {
|
||||
"taskId": "task-5",
|
||||
"status": "input_required",
|
||||
"statusMessage": "Need more data",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:01:00Z",
|
||||
"pollInterval": 2000
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = verifyDeserialization<CreateTaskResult>(McpJson, json)
|
||||
result.task.taskId shouldBe "task-5"
|
||||
result.task.status shouldBe TaskStatus.InputRequired
|
||||
result.task.statusMessage shouldBe "Need more data"
|
||||
result.task.pollInterval shouldBe 2000L
|
||||
result.task.ttl.shouldBeNull()
|
||||
result.meta.shouldBeNull()
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// GetTaskRequest
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize GetTaskRequest`() {
|
||||
val request = GetTaskRequest(
|
||||
GetTaskRequestParams(taskId = "task-10"),
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
request,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"method": "tasks/get",
|
||||
"params": {
|
||||
"taskId": "task-10"
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should serialize GetTaskRequest with meta`() {
|
||||
val request = GetTaskRequest(
|
||||
GetTaskRequestParams(
|
||||
taskId = "task-10",
|
||||
meta = RequestMeta(
|
||||
buildJsonObject { put("progressToken", "pt-1") },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
request,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"method": "tasks/get",
|
||||
"params": {
|
||||
"taskId": "task-10",
|
||||
"_meta": {
|
||||
"progressToken": "pt-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize GetTaskRequest`() {
|
||||
val json = """
|
||||
{
|
||||
"method": "tasks/get",
|
||||
"params": {
|
||||
"taskId": "task-11"
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val request = verifyDeserialization<GetTaskRequest>(McpJson, json)
|
||||
request.method shouldBe Method.Defined.TasksGet
|
||||
request.taskId shouldBe "task-11"
|
||||
request.meta.shouldBeNull()
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// GetTaskResult
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize GetTaskResult with all fields`() {
|
||||
val result = GetTaskResult(
|
||||
taskId = "task-20",
|
||||
status = TaskStatus.Completed,
|
||||
statusMessage = "Done",
|
||||
createdAt = "2025-01-01T00:00:00Z",
|
||||
lastUpdatedAt = "2025-01-01T00:05:00Z",
|
||||
ttl = 300000,
|
||||
pollInterval = 10000,
|
||||
meta = buildJsonObject { put("server", "main") },
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
result,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"taskId": "task-20",
|
||||
"status": "completed",
|
||||
"statusMessage": "Done",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:05:00Z",
|
||||
"ttl": 300000,
|
||||
"pollInterval": 10000,
|
||||
"_meta": {
|
||||
"server": "main"
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize GetTaskResult and verify TaskFields contract`() {
|
||||
val json = """
|
||||
{
|
||||
"taskId": "task-21",
|
||||
"status": "working",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:00:30Z"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = verifyDeserialization<GetTaskResult>(McpJson, json)
|
||||
result.shouldBeInstanceOf<TaskFields>()
|
||||
result.taskId shouldBe "task-21"
|
||||
result.status shouldBe TaskStatus.Working
|
||||
result.statusMessage.shouldBeNull()
|
||||
result.ttl.shouldBeNull()
|
||||
result.pollInterval.shouldBeNull()
|
||||
result.meta.shouldBeNull()
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// GetTaskPayloadRequest
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize GetTaskPayloadRequest`() {
|
||||
val request = GetTaskPayloadRequest(
|
||||
GetTaskPayloadRequestParams(taskId = "task-30"),
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
request,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"method": "tasks/result",
|
||||
"params": {
|
||||
"taskId": "task-30"
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize GetTaskPayloadRequest`() {
|
||||
val json = """
|
||||
{
|
||||
"method": "tasks/result",
|
||||
"params": {
|
||||
"taskId": "task-31"
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val request = verifyDeserialization<GetTaskPayloadRequest>(McpJson, json)
|
||||
request.method shouldBe Method.Defined.TasksResult
|
||||
request.taskId shouldBe "task-31"
|
||||
request.meta.shouldBeNull()
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// GetTaskPayloadResult
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize GetTaskPayloadResult with meta`() {
|
||||
val result = GetTaskPayloadResult(
|
||||
buildJsonObject {
|
||||
put("_meta", buildJsonObject { put("origin", "task-30") })
|
||||
},
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
result,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"_meta": {
|
||||
"origin": "task-30"
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should serialize GetTaskPayloadResult without meta`() {
|
||||
val result = GetTaskPayloadResult(buildJsonObject { })
|
||||
|
||||
verifySerialization(result, McpJson, "{}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should preserve arbitrary payload fields in GetTaskPayloadResult`() {
|
||||
val json = """
|
||||
{
|
||||
"_meta": {
|
||||
"origin": "task-42"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "hello"
|
||||
}
|
||||
],
|
||||
"isError": false
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = verifyDeserialization<GetTaskPayloadResult>(McpJson, json)
|
||||
result.meta.shouldNotBeNull()
|
||||
result.meta?.get("origin")?.jsonPrimitive?.content shouldBe "task-42"
|
||||
result["content"].shouldNotBeNull()
|
||||
result["isError"].shouldNotBeNull()
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// ListTasksRequest
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize ListTasksRequest without params`() {
|
||||
val request = ListTasksRequest()
|
||||
|
||||
verifySerialization(
|
||||
request,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"method": "tasks/list"
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should serialize ListTasksRequest with cursor`() {
|
||||
val request = ListTasksRequest(
|
||||
params = PaginatedRequestParams(cursor = "page-2"),
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
request,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"method": "tasks/list",
|
||||
"params": {
|
||||
"cursor": "page-2"
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize ListTasksRequest`() {
|
||||
val json = """
|
||||
{
|
||||
"method": "tasks/list",
|
||||
"params": {
|
||||
"cursor": "next-page"
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val request = verifyDeserialization<ListTasksRequest>(McpJson, json)
|
||||
request.method shouldBe Method.Defined.TasksList
|
||||
request.cursor shouldBe "next-page"
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// ListTasksResult
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize ListTasksResult with empty list`() {
|
||||
val result = ListTasksResult(tasks = emptyList())
|
||||
|
||||
verifySerialization(
|
||||
result,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"tasks": []
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should serialize ListTasksResult with tasks and pagination`() {
|
||||
val result = ListTasksResult(
|
||||
tasks = listOf(
|
||||
Task(
|
||||
taskId = "task-1",
|
||||
status = TaskStatus.Working,
|
||||
createdAt = "2025-01-01T00:00:00Z",
|
||||
lastUpdatedAt = "2025-01-01T00:00:00Z",
|
||||
ttl = null,
|
||||
),
|
||||
Task(
|
||||
taskId = "task-2",
|
||||
status = TaskStatus.Completed,
|
||||
createdAt = "2025-01-01T00:00:00Z",
|
||||
lastUpdatedAt = "2025-01-01T00:01:00Z",
|
||||
ttl = 60000,
|
||||
),
|
||||
),
|
||||
nextCursor = "cursor-abc",
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
result,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task-1",
|
||||
"status": "working",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"taskId": "task-2",
|
||||
"status": "completed",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:01:00Z",
|
||||
"ttl": 60000
|
||||
}
|
||||
],
|
||||
"nextCursor": "cursor-abc"
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize ListTasksResult`() {
|
||||
val json = """
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "task-99",
|
||||
"status": "cancelled",
|
||||
"statusMessage": "User cancelled",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:03:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = verifyDeserialization<ListTasksResult>(McpJson, json)
|
||||
result.tasks.size shouldBe 1
|
||||
result.tasks[0].taskId shouldBe "task-99"
|
||||
result.tasks[0].status shouldBe TaskStatus.Cancelled
|
||||
result.tasks[0].statusMessage shouldBe "User cancelled"
|
||||
result.tasks[0].ttl.shouldBeNull()
|
||||
result.nextCursor.shouldBeNull()
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// CancelTaskRequest
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize CancelTaskRequest`() {
|
||||
val request = CancelTaskRequest(
|
||||
CancelTaskRequestParams(taskId = "task-40"),
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
request,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"method": "tasks/cancel",
|
||||
"params": {
|
||||
"taskId": "task-40"
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize CancelTaskRequest`() {
|
||||
val json = """
|
||||
{
|
||||
"method": "tasks/cancel",
|
||||
"params": {
|
||||
"taskId": "task-41"
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val request = verifyDeserialization<CancelTaskRequest>(McpJson, json)
|
||||
request.method shouldBe Method.Defined.TasksCancel
|
||||
request.taskId shouldBe "task-41"
|
||||
request.meta.shouldBeNull()
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// CancelTaskResult
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should serialize CancelTaskResult`() {
|
||||
val result = CancelTaskResult(
|
||||
taskId = "task-40",
|
||||
status = TaskStatus.Cancelled,
|
||||
statusMessage = "Cancelled by user",
|
||||
createdAt = "2025-01-01T00:00:00Z",
|
||||
lastUpdatedAt = "2025-01-01T00:02:00Z",
|
||||
ttl = null,
|
||||
)
|
||||
|
||||
verifySerialization(
|
||||
result,
|
||||
McpJson,
|
||||
"""
|
||||
{
|
||||
"taskId": "task-40",
|
||||
"status": "cancelled",
|
||||
"statusMessage": "Cancelled by user",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:02:00Z"
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should deserialize CancelTaskResult and verify TaskFields contract`() {
|
||||
val json = """
|
||||
{
|
||||
"taskId": "task-42",
|
||||
"status": "cancelled",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:04:00Z",
|
||||
"ttl": 120000,
|
||||
"pollInterval": 3000
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = verifyDeserialization<CancelTaskResult>(McpJson, json)
|
||||
result.shouldBeInstanceOf<TaskFields>()
|
||||
result.taskId shouldBe "task-42"
|
||||
result.status shouldBe TaskStatus.Cancelled
|
||||
result.statusMessage.shouldBeNull()
|
||||
result.ttl shouldBe 120000L
|
||||
result.pollInterval shouldBe 3000L
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Deserialization with unknown fields (ignoreUnknownKeys)
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should deserialize Task ignoring unknown fields`() {
|
||||
val json = """
|
||||
{
|
||||
"taskId": "task-100",
|
||||
"status": "working",
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"lastUpdatedAt": "2025-01-01T00:00:00Z",
|
||||
"unknownField": "should be ignored"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val task = McpJson.decodeFromString<Task>(json)
|
||||
task.taskId shouldBe "task-100"
|
||||
task.status shouldBe TaskStatus.Working
|
||||
task.ttl.shouldBeNull()
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// RequestMeta.relatedTask accessor
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
fun `should read relatedTask from RequestMeta`() {
|
||||
val meta = RequestMeta(
|
||||
buildJsonObject {
|
||||
put(
|
||||
RELATED_TASK_META_KEY,
|
||||
buildJsonObject { put("taskId", "task-50") },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
val related = meta.relatedTask
|
||||
related.shouldNotBeNull()
|
||||
related.taskId shouldBe "task-50"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return null relatedTask when key is absent`() {
|
||||
val meta = RequestMeta(
|
||||
buildJsonObject { put("progressToken", "pt-1") },
|
||||
)
|
||||
|
||||
meta.relatedTask.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GetTaskResult cast to CancelTaskResult via generic does not throw due to type erasure`() {
|
||||
val result: RequestResult = GetTaskResult(
|
||||
taskId = "task-1",
|
||||
status = TaskStatus.Cancelled,
|
||||
createdAt = "2025-01-01T00:00:00Z",
|
||||
lastUpdatedAt = "2025-01-01T00:01:00Z",
|
||||
ttl = null,
|
||||
)
|
||||
|
||||
// Simulates what Protocol.request<T>() does:
|
||||
// the cast goes through a generic type parameter, so JVM erases T to RequestResult
|
||||
fun <T : RequestResult> uncheckedCast(value: RequestResult): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return value as T
|
||||
}
|
||||
|
||||
val cancelResult: CancelTaskResult = uncheckedCast(result)
|
||||
|
||||
cancelResult.taskId shouldBe "task-1"
|
||||
cancelResult.status shouldBe TaskStatus.Cancelled
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue