Skip to content
564 changes: 564 additions & 0 deletions kotlin-sdk-core/api/kotlin-sdk-core.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,18 @@ 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"),
SamplingCreateMessage("sampling/createMessage"),
CompletionComplete("completion/complete"),
RootsList("roots/list"),
ElicitationCreate("elicitation/create"),
TasksGet("tasks/get"),
TasksResult("tasks/result"),
TasksList("tasks/list"),
TasksCancel("tasks/cancel"),
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Comment thread
devcrocod marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we cache it or handle during deserialization in the serializer? Currently, it will run json parsing every time

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json is not parsed every time, It’s not an expensive operation

I can do it that way, but then we’ll have to modify the class since it’s value class

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handle during deserialization in the serializer

And that would require a custom serializer, which I’d prefer to avoid

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json is not parsed every time, It’s not an expensive operation

I see the getter is not cached, so every property call will result in json parsing. In general, it should be avoided. The application would have to cache it.

A value class may not be the best solution here, as it introduces unnecessary restrictions.

Additionally, RequestMeta risks becoming a God Object as more metadata (e.g., RelatedTaskMetadata) is added over time. This pattern hinders extensibility and violates clean design principles.

An alternative solution is to use an extension function:

public fun RequestMeta.relatedTaskMetadata(): RelatedTaskMetadata? = this.json[RELATED_TASK_META_KEY]?.let {
    McpJson.decodeFromJsonElement(it)
}

This approach is more extensible and avoids the pitfalls of the current solution, even if it doesn't address the lack of caching.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the getter is not cached, so every property call will result in json parsing. In general, it should be avoided. The application would have to cache it.

To clarify, decodeFromJsonElement doesn't parse a json string. The JsonObject tree is already built at that point, so this call simply maps an existing JsonElement to a RelatedTaskMetadata(taskId: String), essentially one string read from an in-memory tree. It's a very lightweight operation.

I see the getter is not cached, so every property call will result in json parsing. In general, it should be avoided. The application would have to cache it.

RequestMeta currently has only two properties: progressToken and relatedTask. Both are defined by the MCP spec, not arbitrary extensions. I don't think two spec-defined accessors qualify as a God Object pattern

The relatedTask property was intentionally implemented to be consistent with progressToken, which is already defined as a computed property on RequestMeta in the same style. Moving one to an extension function while keeping the other as a property would break that consistency

}

/**
* Retrieves the value associated with the specified key from the JSON object.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
}

Expand All @@ -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(),
)
}

Expand Down Expand Up @@ -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(),
)
}

Expand All @@ -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(),
)
}

Expand Down Expand Up @@ -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()
Comment thread
kpavlov marked this conversation as resolved.
"tasks" in jsonObject -> ListTasksResult.serializer()
"taskId" in jsonObject -> GetTaskResult.serializer()
Comment thread
devcrocod marked this conversation as resolved.
else -> null
}
}
Expand All @@ -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 {
Expand All @@ -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
}
}
Expand Down
Loading
Loading