Skip to content

feat: add tasks models#566

Open
devcrocod wants to merge 6 commits intomainfrom
devcrocod/tasks-classes
Open

feat: add tasks models#566
devcrocod wants to merge 6 commits intomainfrom
devcrocod/tasks-classes

Conversation

@devcrocod
Copy link
Contributor

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)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

@devcrocod devcrocod requested a review from Amaneusz February 26, 2026 15:18
@devcrocod devcrocod marked this pull request as ready for review February 26, 2026 15:18
Copilot AI review requested due to automatic review settings February 26, 2026 15:18
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds initial Kotlin SDK type support for MCP “tasks” (SEP-1686) by introducing task models, task-related request/response/notification types, and wiring them into the existing JSON (de)serialization layer.

Changes:

  • Introduces core task types (Task, TaskStatus, TaskMetadata, RelatedTaskMetadata) and task RPC types (tasks/get, tasks/result, tasks/list, tasks/cancel) plus notifications/tasks/status.
  • Registers new methods and hooks task requests/notifications/results into the polymorphic serializers.
  • Adds/extends unit tests for task serialization/deserialization and updates detekt baselines / public API snapshot.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/tasks.kt New task models + task RPC request/result types.
kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/notification.kt Adds TaskStatusNotification and params.
kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt Registers new task methods + notification method.
kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt Wires task request/notification deserialization and result shape matching.
kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/request.kt Adds RequestMeta.relatedTask accessor.
kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/TasksTest.kt New comprehensive tests for task types and RPCs.
kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/NotificationTest.kt Adds tests for notifications/tasks/status.
kotlin-sdk-core/detekt-baseline-*.xml Baseline updates/suppressions for new code/tests.
kotlin-sdk-core/api/kotlin-sdk-core.api Updates public API snapshot for new types.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@kpavlov kpavlov left a comment

Choose a reason for hiding this comment

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

Thank you, @devcrocod
There are copilot findings that look legitimate.
Let's split big classes and don't inflate detekt baselines.

<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>LargeClass:TasksTest.kt:TasksTest</ID>
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's not inflate detekt baselines.
In this case the test class should be splitted

Copy link
Contributor Author

Choose a reason for hiding this comment

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

*/
public val relatedTask: RelatedTaskMetadata?
get() = json[RELATED_TASK_META_KEY]?.let { element ->
McpJson.decodeFromJsonElement(element)
Copy link
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
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
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
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.

ServerResult,
TaskFields

// ============================================================================
Copy link
Contributor

Choose a reason for hiding this comment

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

This file is getting huge. Let's split it. There are clear boundaries: models, requests, responses

<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>CyclomaticComplexMethod:jsonUtils.kt:@OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) private fun convertToJsonElement: JsonElement</ID>
<ID>CyclomaticComplexMethod:serializers.kt:private fun selectServerResultDeserializer: DeserializationStrategy&lt;ServerResult>?</ID>
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we address it in a proper way?

@devcrocod devcrocod force-pushed the devcrocod/tasks-classes branch from f43c48b to cb0c75b Compare March 2, 2026 18:20
@codecov-commenter
Copy link

Codecov Report

❌ Patch coverage is 79.81651% with 22 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
.../io/modelcontextprotocol/kotlin/sdk/types/tasks.kt 81.08% 0 Missing and 14 partials ⚠️
...delcontextprotocol/kotlin/sdk/types/serializers.kt 62.50% 0 Missing and 6 partials ⚠️
...elcontextprotocol/kotlin/sdk/types/notification.kt 83.33% 0 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI review requested due to automatic review settings March 2, 2026 19:00
@devcrocod devcrocod requested a review from kpavlov March 2, 2026 19:01
Comment on lines +472 to +475
assertNotNull(result.meta)
assertEquals("task-42", result.meta?.get("origin")?.jsonPrimitive?.content)
assertNotNull(result["content"])
assertNotNull(result["isError"])
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not give Kotest a try for our new tests?


assertEquals("task-1", cancelResult.taskId)
assertEquals(TaskStatus.Cancelled, cancelResult.status)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Wow, that’s almost 800 lines!

How about we break things down a bit? We could split tasks.kt and TaskTest.kt into different files, like GetTask.kt, ListTasks.kt and CancelTask.kt, based on what each one does.

@SerialName("_meta")
override val meta: JsonObject? = null,
) : ServerResult
public value class GetTaskPayloadResult(public val json: JsonObject) :
Copy link
Contributor

@kpavlov kpavlov Mar 2, 2026

Choose a reason for hiding this comment

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

I am not sure I understand the advantages of moving to the value class here. It seems like it might not be possible to extend it by the user.
@e5l, do you think we are safe here?

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +55 to +63
/**
* 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: Double? = null)

Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

PR description says this change “closes #421 / #406”, but this PR appears to only add task-related model/types, serializers, and tests. Key items called out in those issues (e.g., updating LATEST_PROTOCOL_VERSION to 2025-11-25 and adding task: TaskMetadata? to RequestParams/request params) are not included here, so merging this as-is likely shouldn’t auto-close those tracking issues. Consider updating the PR description/linked issues to reflect the actual scope (e.g., “partial” / “follow-ups”).

Copilot uses AI. Check for mistakes.
@kpavlov
Copy link
Contributor

kpavlov commented Mar 3, 2026

@devcrocod, please consider defining a public typealias TaskId = String or value class for type safety and, potentially, validation


@Test
fun `RELATED_TASK_META_KEY should have correct value`() {
assertEquals("io.modelcontextprotocol/related-task", RELATED_TASK_META_KEY)
Copy link
Contributor

Choose a reason for hiding this comment

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

This test is not valuable. Let's verify RequestMeta deserialization with RelatedTaskMetadata instead.

Would it be possible to get some test data from the spec? https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks, e.g. https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#retrieving-task-results response is pretty good.

{
"method": "tasks/get",
"params": {
"taskId": "task-11"
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we missing the "io.modelcontextprotocol/related-task" here?

5.7.1.​
Associating Task-Related Messages
All requests, notifications, and responses related to a task MUST include the io.modelcontextprotocol/related-task key in their _meta field, with the value set to an object with a taskId matching the associated task ID.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement SEP-1686: Tasks

5 participants