feat(core): add SEP-2575 request metadata and discovery types (#893)

## Summary

Part of #815.

Adds the core types and codecs needed for SEP-2575:

- experimental accessors for request-scoped metadata while preserving
legacy request shapes and unknown `_meta` entries;
- `server/discover` request/result types, cache fields, and
unsupported-version error data; and
- polymorphic request/result decoding with wire-format and
malformed-input coverage.

The discovery types match the merged draft schema in
modelcontextprotocol/modelcontextprotocol#3002: `protocolVersion` and
`clientCapabilities` are required, `clientInfo` is optional, and
optional server information remains in result `_meta`.

## Motivation and Context

SEP-2575 moves protocol and client context onto individual requests and
introduces `server/discover`. The shared `RequestMeta` accessors remain
nullable so legacy requests stay representable; `DiscoverRequestParams`
enforces the fields required by the new discovery request.

This PR stops at the core type and codec boundary. It does not add
server admission, request-scoped dispatch, Streamable HTTP,
authentication, subscriptions, MRTR, lifecycle hooks, or a discovery
DSL.

## How Has This Been Tested?

- JVM: 597/597 tests
- JS Node and Chrome: 528/528 tests per target
- WasmJS Node and Chrome: 528/528 tests per target
- iOS Simulator arm64 and macOS arm64: 528/528 tests per target
- API dump/check, ktlint, detekt, and diff checks

Coverage includes required discovery metadata, optional `clientInfo`,
required cache fields, unknown metadata round-trips, the legacy
`resultType` fallback, unsupported-version data, and polymorphic result
classification.

## Breaking Changes

This adds `DiscoverRequest` as a new `ClientRequest` subtype. Consumers
recompiling exhaustive `when` expressions over `ClientRequest` may need
to add a branch. Existing request types and wire representations are
unchanged.

## 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

## Additional context

`ttlMs` and `cacheScope` have Kotlin construction defaults but remain
required when decoding. An absent `resultType` decodes as `"complete"`
for compatibility with legacy results. Discovery result selection
requires the complete required shape so unrelated result extensions are
not misclassified.

---------

Co-authored-by: Pavel Gorgulov <devcrocod@gmail.com>
This commit is contained in:
Yusuf İhsan Görgel 2026-08-06 00:20:07 +03:00 • committed by GitHub
commit 76c12274ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 782 additions and 0 deletions

View file

@ -390,6 +390,19 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/BooleanSchema$Compan
public final fun serializer ()Lkotlinx/serialization/KSerializer; public final fun serializer ()Lkotlinx/serialization/KSerializer;
} }
public final class io/modelcontextprotocol/kotlin/sdk/types/CacheScope : java/lang/Enum {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope$Companion;
public static final field Private Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope;
public static final field Public Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope;
public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope;
}
public final class io/modelcontextprotocol/kotlin/sdk/types/CacheScope$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest { public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest$Companion; public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest$Companion;
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;)V public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;)V
@ -1395,6 +1408,104 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CustomRequest$Compan
public final fun serializer ()Lkotlinx/serialization/KSerializer; public final fun serializer ()Lkotlinx/serialization/KSerializer;
} }
public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest$Companion;
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;)V
public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;
public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest;
public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest;Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest;
public fun equals (Ljava/lang/Object;)Z
public final fun getMeta--Khlwt8 ()Lkotlinx/serialization/json/JsonObject;
public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method;
public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;
public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest$$serializer;
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams$Companion;
public synthetic fun <init> (Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1--Khlwt8 ()Lkotlinx/serialization/json/JsonObject;
public final fun copy-hVVZdjo (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;
public static synthetic fun copy-hVVZdjo$default (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;
public fun equals (Ljava/lang/Object;)Z
public fun getMeta--Khlwt8 ()Lkotlinx/serialization/json/JsonObject;
public synthetic fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams$$serializer;
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverResult : io/modelcontextprotocol/kotlin/sdk/types/ServerResult {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult$Companion;
public fun <init> (Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Ljava/lang/String;Ljava/lang/String;JLio/modelcontextprotocol/kotlin/sdk/types/CacheScope;Lkotlinx/serialization/json/JsonObject;)V
public synthetic fun <init> (Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Ljava/lang/String;Ljava/lang/String;JLio/modelcontextprotocol/kotlin/sdk/types/CacheScope;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/util/List;
public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()J
public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope;
public final fun component7 ()Lkotlinx/serialization/json/JsonObject;
public final fun copy (Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Ljava/lang/String;Ljava/lang/String;JLio/modelcontextprotocol/kotlin/sdk/types/CacheScope;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult;
public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Ljava/lang/String;Ljava/lang/String;JLio/modelcontextprotocol/kotlin/sdk/types/CacheScope;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult;
public fun equals (Ljava/lang/Object;)Z
public final fun getCacheScope ()Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope;
public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;
public final fun getInstructions ()Ljava/lang/String;
public fun getMeta ()Lkotlinx/serialization/json/JsonObject;
public final fun getResultType ()Ljava/lang/String;
public final fun getSupportedVersions ()Ljava/util/List;
public final fun getTtlMs ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/DiscoverResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult$$serializer;
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverResult$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class io/modelcontextprotocol/kotlin/sdk/types/DoubleSchema : io/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition { public final class io/modelcontextprotocol/kotlin/sdk/types/DoubleSchema : io/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema$Companion; public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema$Companion;
public fun <init> ()V public fun <init> ()V
@ -3245,6 +3356,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Defined : jav
public static final field ResourcesUnsubscribe Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field ResourcesUnsubscribe Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
public static final field RootsList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field RootsList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
public static final field SamplingCreateMessage Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field SamplingCreateMessage Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
public static final field ServerDiscover Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
public static final field TasksCancel Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field TasksCancel Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
public static final field TasksGet Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field TasksGet Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
public static final field TasksList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined; public static final field TasksList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
@ -3767,6 +3879,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RPCError$ErrorCode {
public static final field PARSE_ERROR I public static final field PARSE_ERROR I
public static final field REQUEST_TIMEOUT I public static final field REQUEST_TIMEOUT I
public static final field RESOURCE_NOT_FOUND I public static final field RESOURCE_NOT_FOUND I
public static final field UNSUPPORTED_PROTOCOL_VERSION I
public static final field URL_ELICITATION_REQUIRED I public static final field URL_ELICITATION_REQUIRED I
} }
@ -4012,8 +4125,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RequestMeta {
public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z
public static final fun get-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;)Lkotlinx/serialization/json/JsonElement; public static final fun get-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;)Lkotlinx/serialization/json/JsonElement;
public static final fun getClientCapabilities-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;
public static final fun getClientInfo-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;
public final fun getJson ()Lkotlinx/serialization/json/JsonObject; public final fun getJson ()Lkotlinx/serialization/json/JsonObject;
public static final fun getLogLevel-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;
public static final fun getProgressToken-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; public static final fun getProgressToken-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;
public static final fun getProtocolVersion-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String;
public static final fun getRelatedTask-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata; public static final fun getRelatedTask-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata;
public fun hashCode ()I public fun hashCode ()I
public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I
@ -5736,6 +5853,35 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestPa
public final fun serializer ()Lkotlinx/serialization/KSerializer; public final fun serializer ()Lkotlinx/serialization/KSerializer;
} }
public final class io/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData$Companion;
public fun <init> (Ljava/util/List;Ljava/lang/String;)V
public final fun component1 ()Ljava/util/List;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/util/List;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData;
public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData;
public fun equals (Ljava/lang/Object;)Z
public final fun getRequested ()Ljava/lang/String;
public final fun getSupported ()Ljava/util/List;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData$$serializer;
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class io/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema { public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Companion; public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;)V public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;)V

View file

@ -0,0 +1,107 @@
package io.modelcontextprotocol.kotlin.sdk.types
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Required
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.JsonObject
/**
* Parameters for a `server/discover` request.
*
* Discovery belongs to the request-scoped lifecycle and therefore requires a protocol version and
* client capabilities in [meta]. Client information is recommended by the protocol but optional.
* Unknown metadata entries are preserved by [RequestMeta].
*
* @property meta request-scoped protocol and client metadata
* @throws IllegalArgumentException if required request-scoped metadata is absent
* @throws SerializationException if required metadata is malformed
*/
@Serializable
@ExperimentalMcpApi
public data class DiscoverRequestParams(
@SerialName("_meta")
override val meta: RequestMeta,
) : RequestParams {
init {
requireNotNull(meta.protocolVersion) { "${RequestMetaKeys.PROTOCOL_VERSION} is required" }
requireNotNull(meta.clientCapabilities) { "${RequestMetaKeys.CLIENT_CAPABILITIES} is required" }
}
}
/**
* Requests the protocol versions, capabilities, and metadata advertised by a server.
*
* @property params request metadata required by the request-scoped lifecycle
*/
@Serializable
@ExperimentalMcpApi
public data class DiscoverRequest(override val params: DiscoverRequestParams) : ClientRequest {
@OptIn(ExperimentalSerializationApi::class)
@EncodeDefault
override val method: Method = Method.Defined.ServerDiscover
/** Metadata for this request. */
public val meta: RequestMeta
get() = params.meta
}
/** Defines where a cached discovery result may be reused. */
@Serializable
@ExperimentalMcpApi
public enum class CacheScope {
/** The result may only be reused within the same authorization context. */
@SerialName("private")
Private,
/** The result may be shared across authorization contexts. */
@SerialName("public")
Public,
}
/**
* Result returned by `server/discover`.
*
* @property supportedVersions protocol versions supported by the server
* @property capabilities capabilities advertised by the server
* @property instructions optional guidance for clients using the server
* @property resultType discriminator for the result representation
* @property ttlMs number of milliseconds clients may treat this result as fresh
* @property cacheScope authorization boundary within which the result may be reused
* @property meta optional response metadata, including optional server information
* @throws IllegalArgumentException if [ttlMs] is negative
*/
@Serializable
@ExperimentalMcpApi
public data class DiscoverResult(
val supportedVersions: List<String>,
val capabilities: ServerCapabilities,
val instructions: String? = null,
@EncodeDefault
val resultType: String = COMPLETE_RESULT_TYPE,
@Required
val ttlMs: Long = 0,
@Required
val cacheScope: CacheScope = CacheScope.Private,
@SerialName("_meta")
override val meta: JsonObject? = null,
) : ServerResult {
init {
require(ttlMs >= 0) { "ttlMs must be non-negative, but was $ttlMs" }
}
}
/**
* Data returned when a request selects an unsupported protocol version.
*
* @property supported protocol versions supported by the receiver
* @property requested protocol version selected by the request
*/
@Serializable
@ExperimentalMcpApi
public data class UnsupportedProtocolVersionData(val supported: List<String>, val requested: String)
internal const val COMPLETE_RESULT_TYPE: String = "complete"

View file

@ -269,6 +269,9 @@ public data class RPCError(val code: Int, val message: String, val data: JsonEle
/** Resource not found */ /** Resource not found */
public const val RESOURCE_NOT_FOUND: Int = -32002 public const val RESOURCE_NOT_FOUND: Int = -32002
/** The request selected a protocol version that the receiver does not support. */
public const val UNSUPPORTED_PROTOCOL_VERSION: Int = -32022
/** /**
* A URL-mode elicitation must be completed before the request can be processed. * A URL-mode elicitation must be completed before the request can be processed.
* The error [data][RPCError.data] carries the required URL-mode elicitations * The error [data][RPCError.data] carries the required URL-mode elicitations

View file

@ -16,6 +16,7 @@ public sealed interface Method {
@Serializable @Serializable
public enum class Defined(override val value: String) : Method { public enum class Defined(override val value: String) : Method {
Initialize("initialize"), Initialize("initialize"),
ServerDiscover("server/discover"),
Ping("ping"), Ping("ping"),
ResourcesList("resources/list"), ResourcesList("resources/list"),
ResourcesTemplatesList("resources/templates/list"), ResourcesTemplatesList("resources/templates/list"),

View file

@ -1,8 +1,10 @@
package io.modelcontextprotocol.kotlin.sdk.types package io.modelcontextprotocol.kotlin.sdk.types
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
@ -11,6 +13,13 @@ import kotlinx.serialization.json.long
import kotlinx.serialization.json.longOrNull import kotlinx.serialization.json.longOrNull
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
internal object RequestMetaKeys {
const val PROTOCOL_VERSION: String = "io.modelcontextprotocol/protocolVersion"
const val CLIENT_INFO: String = "io.modelcontextprotocol/clientInfo"
const val CLIENT_CAPABILITIES: String = "io.modelcontextprotocol/clientCapabilities"
const val LOG_LEVEL: String = "io.modelcontextprotocol/logLevel"
}
/** /**
* Metadata attached to a request's `_meta` field. * Metadata attached to a request's `_meta` field.
* *
@ -40,6 +49,88 @@ public value class RequestMeta(public val json: JsonObject) {
McpJson.decodeFromJsonElement(element) McpJson.decodeFromJsonElement(element)
} }
/**
* The MCP protocol version selected for this request, or `null` when absent.
*
* This field is required by the request-scoped lifecycle introduced in protocol version
* `2026-07-28`, but remains optional here so older requests retain their wire shape.
*
* @throws SerializationException if the field is present but is not a string
*/
@ExperimentalMcpApi
public val protocolVersion: String?
get() = json[RequestMetaKeys.PROTOCOL_VERSION]?.let { element ->
if (element !is JsonPrimitive || !element.isString) {
throw SerializationException("${RequestMetaKeys.PROTOCOL_VERSION} must be a JSON string")
}
element.content
}
/**
* Information about the client making this request, or `null` when absent.
*
* Request-scoped clients should include this field unless specifically configured not to do
* so. The self-reported value is intended for display, logging, and debugging; servers should
* not use it to change behavior or rely on it for security decisions.
*
* @throws SerializationException if the field is present but is not a valid [Implementation]
*/
@ExperimentalMcpApi
public val clientInfo: Implementation?
get() = json[RequestMetaKeys.CLIENT_INFO]?.let { element ->
try {
McpJson.decodeFromJsonElement<Implementation>(element)
} catch (cause: SerializationException) {
throw SerializationException(
"${RequestMetaKeys.CLIENT_INFO} must be a valid client implementation",
cause,
)
}
}
/**
* Capabilities declared by the client for this request, or `null` when absent.
*
* Servers using the request-scoped lifecycle must not infer capabilities from an earlier
* request. An empty object means the client supports no optional capabilities.
*
* @throws SerializationException if the field is present but is not valid [ClientCapabilities]
*/
@ExperimentalMcpApi
public val clientCapabilities: ClientCapabilities?
get() = json[RequestMetaKeys.CLIENT_CAPABILITIES]?.let { element ->
try {
McpJson.decodeFromJsonElement<ClientCapabilities>(element)
} catch (cause: SerializationException) {
throw SerializationException(
"${RequestMetaKeys.CLIENT_CAPABILITIES} must be a valid client capabilities object",
cause,
)
}
}
/**
* Minimum severity of request-associated log notifications, or `null` when absent.
*
* In the request-scoped lifecycle, absence means that the server must not emit log
* notifications for this request.
*
* @throws SerializationException if the field is present but is not a valid [LoggingLevel]
*/
@Deprecated("Per-request log levels are deprecated as of MCP protocol version 2026-07-28 (SEP-2577).")
@ExperimentalMcpApi
public val logLevel: LoggingLevel?
get() = json[RequestMetaKeys.LOG_LEVEL]?.let { element ->
try {
McpJson.decodeFromJsonElement<LoggingLevel>(element)
} catch (cause: SerializationException) {
throw SerializationException(
"${RequestMetaKeys.LOG_LEVEL} must be a valid logging level",
cause,
)
}
}
/** /**
* Retrieves the value associated with the specified key from the JSON object. * Retrieves the value associated with the specified key from the JSON object.
* *

View file

@ -1,6 +1,7 @@
package io.modelcontextprotocol.kotlin.sdk.types package io.modelcontextprotocol.kotlin.sdk.types
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
@ -238,9 +239,11 @@ internal object ResourceContentsPolymorphicSerializer :
// Request Serializers // Request Serializers
// ============================================================================ // ============================================================================
@OptIn(ExperimentalMcpApi::class)
private val clientRequestDeserializers: Map<String, DeserializationStrategy<ClientRequest>> by lazy { private val clientRequestDeserializers: Map<String, DeserializationStrategy<ClientRequest>> by lazy {
mapOf( mapOf(
Method.Defined.CompletionComplete.value to CompleteRequest.serializer(), Method.Defined.CompletionComplete.value to CompleteRequest.serializer(),
Method.Defined.ServerDiscover.value to DiscoverRequest.serializer(),
Method.Defined.Initialize.value to InitializeRequest.serializer(), Method.Defined.Initialize.value to InitializeRequest.serializer(),
Method.Defined.Ping.value to PingRequest.serializer(), Method.Defined.Ping.value to PingRequest.serializer(),
Method.Defined.LoggingSetLevel.value to SetLevelRequest.serializer(), Method.Defined.LoggingSetLevel.value to SetLevelRequest.serializer(),
@ -417,9 +420,16 @@ private fun selectClientResultDeserializer(element: JsonElement): Deserializatio
* Selects the appropriate deserializer for server results based on JSON content. * Selects the appropriate deserializer for server results based on JSON content.
* Returns null if the structure doesn't match any known server result type. * Returns null if the structure doesn't match any known server result type.
*/ */
@OptIn(ExperimentalMcpApi::class)
private fun selectServerResultDeserializer(element: JsonElement): DeserializationStrategy<ServerResult>? { private fun selectServerResultDeserializer(element: JsonElement): DeserializationStrategy<ServerResult>? {
val jsonObject = element.jsonObject val jsonObject = element.jsonObject
val isDiscoverResult =
"supportedVersions" in jsonObject &&
"capabilities" in jsonObject &&
"ttlMs" in jsonObject &&
"cacheScope" in jsonObject
return when { return when {
isDiscoverResult -> DiscoverResult.serializer()
"protocolVersion" in jsonObject && "capabilities" in jsonObject -> InitializeResult.serializer() "protocolVersion" in jsonObject && "capabilities" in jsonObject -> InitializeResult.serializer()
"completion" in jsonObject -> CompleteResult.serializer() "completion" in jsonObject -> CompleteResult.serializer()
"tools" in jsonObject -> ListToolsResult.serializer() "tools" in jsonObject -> ListToolsResult.serializer()

View file

@ -0,0 +1,324 @@
package io.modelcontextprotocol.kotlin.sdk.types
import io.kotest.assertions.json.shouldEqualJson
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertFailsWith
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@OptIn(ExperimentalMcpApi::class)
class DiscoveryTest {
@Test
fun `should decode server discover request from its wire shape`() {
val request = McpJson.decodeFromString<Request>(
"""
{
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "test-client",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
""".trimIndent(),
)
val discover = assertIs<DiscoverRequest>(request)
assertEquals(Method.Defined.ServerDiscover, discover.method)
assertEquals("2026-07-28", discover.meta.protocolVersion)
assertEquals("test-client", discover.meta.clientInfo?.name)
assertEquals(ClientCapabilities(), discover.meta.clientCapabilities)
}
@Test
fun `should decode discovery request without optional client info`() {
val request = McpJson.decodeFromString<Request>(
"""
{
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"com.example/traceId": "trace-123"
}
}
}
""".trimIndent(),
)
val discover = assertIs<DiscoverRequest>(request)
assertEquals("2026-07-28", discover.meta.protocolVersion)
assertEquals(ClientCapabilities(), discover.meta.clientCapabilities)
assertNull(discover.meta.clientInfo)
assertNotNull(discover.meta["com.example/traceId"])
McpJson.encodeToString<Request>(discover) shouldEqualJson """
{
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"com.example/traceId": "trace-123"
}
}
}
""".trimIndent()
}
@Test
fun `should reject discovery requests without required request metadata`() {
val invalidParams = listOf(
"{}" to "_meta",
"""{"_meta": {}}""" to RequestMetaKeys.PROTOCOL_VERSION,
"""
{
"_meta": {
"${RequestMetaKeys.PROTOCOL_VERSION}": "2026-07-28",
"${RequestMetaKeys.CLIENT_INFO}": {
"name": "test-client",
"version": "1.0.0"
}
}
}
""".trimIndent() to RequestMetaKeys.CLIENT_CAPABILITIES,
)
invalidParams.forEach { (params, missingField) ->
val failure = assertFails {
McpJson.decodeFromString<Request>(
"""{"method": "server/discover", "params": $params}""",
)
}
assertTrue(
failure.message.orEmpty().contains(missingField),
"Expected failure to identify missing field $missingField, but was: $failure",
)
}
}
@Test
fun `should serialize every required discovery result field`() {
val result = DiscoverResult(
supportedVersions = listOf("2026-07-28"),
capabilities = ServerCapabilities(
tools = ServerCapabilities.Tools(listChanged = true),
),
instructions = "Use tools intentionally.",
)
McpJson.encodeToString<ServerResult>(result) shouldEqualJson """
{
"supportedVersions": ["2026-07-28"],
"capabilities": {
"tools": {"listChanged": true}
},
"instructions": "Use tools intentionally.",
"resultType": "complete",
"ttlMs": 0,
"cacheScope": "private"
}
""".trimIndent()
}
@Test
fun `should encode optional server info in result metadata`() {
val result = DiscoverResult(
supportedVersions = listOf("2026-07-28"),
capabilities = ServerCapabilities(),
meta = Json.parseToJsonElement(
"""
{
"io.modelcontextprotocol/serverInfo": {
"name": "test-server",
"version": "2.0.0"
},
"com.example/source": "edge"
}
""".trimIndent(),
).jsonObject,
)
McpJson.encodeToString<ServerResult>(result) shouldEqualJson """
{
"supportedVersions": ["2026-07-28"],
"capabilities": {},
"resultType": "complete",
"ttlMs": 0,
"cacheScope": "private",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "test-server",
"version": "2.0.0"
},
"com.example/source": "edge"
}
}
""".trimIndent()
assertEquals(
Json.parseToJsonElement("""{"name":"test-server","version":"2.0.0"}"""),
result.meta?.get("io.modelcontextprotocol/serverInfo"),
)
}
@Test
fun `should encode required defaults independently of McpJson`() {
val result = DiscoverResult(
supportedVersions = listOf("2026-07-28"),
capabilities = ServerCapabilities(),
)
Json.encodeToString(result) shouldEqualJson """
{
"supportedVersions": ["2026-07-28"],
"capabilities": {},
"resultType": "complete",
"ttlMs": 0,
"cacheScope": "private"
}
""".trimIndent()
}
@Test
fun `should reject discovery results without required cache fields`() {
val invalidResults = listOf(
"""
{
"supportedVersions": ["2026-07-28"],
"capabilities": {},
"cacheScope": "private"
}
""".trimIndent(),
"""
{
"supportedVersions": ["2026-07-28"],
"capabilities": {},
"ttlMs": 0
}
""".trimIndent(),
)
invalidResults.forEach { wire ->
assertFails { McpJson.decodeFromString<DiscoverResult>(wire) }
assertFails { McpJson.decodeFromString<ServerResult>(wire) }
}
}
@Test
fun `should use complete when a discovery result omits resultType`() {
val result = McpJson.decodeFromString<ServerResult>(
"""
{
"supportedVersions": ["2026-07-28"],
"capabilities": {},
"ttlMs": 0,
"cacheScope": "private"
}
""".trimIndent(),
)
assertEquals(COMPLETE_RESULT_TYPE, assertIs<DiscoverResult>(result).resultType)
}
@Test
fun `should deserialize discovery results polymorphically`() {
val wire =
"""
{
"supportedVersions": ["2026-07-28"],
"capabilities": {},
"resultType": "complete",
"ttlMs": 250,
"cacheScope": "public",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "server",
"version": "1"
},
"com.example/source": "edge"
}
}
""".trimIndent()
val result = McpJson.decodeFromString<ServerResult>(wire)
val discover = assertIs<DiscoverResult>(result)
assertEquals(250, discover.ttlMs)
assertEquals(CacheScope.Public, discover.cacheScope)
assertEquals(
Json.parseToJsonElement("""{"name":"server","version":"1"}"""),
discover.meta?.get("io.modelcontextprotocol/serverInfo"),
)
assertNotNull(discover.meta?.get("com.example/source"))
McpJson.encodeToString<ServerResult>(discover) shouldEqualJson wire
}
@Test
fun `should not treat supportedVersions alone as a discovery discriminator`() {
val result = McpJson.decodeFromString<ServerResult>(
"""
{
"tools": [],
"supportedVersions": ["extension-value"]
}
""".trimIndent(),
)
assertIs<ListToolsResult>(result)
}
@Test
fun `should not treat an initialize extension as a discovery result`() {
val result = McpJson.decodeFromString<ServerResult>(
"""
{
"protocolVersion": "2025-06-18",
"capabilities": {},
"serverInfo": {
"name": "test-server",
"version": "1.0.0"
},
"supportedVersions": ["extension-value"]
}
""".trimIndent(),
)
assertIs<InitializeResult>(result)
}
@Test
fun `should reject a negative discovery ttl`() {
assertFailsWith<IllegalArgumentException> {
DiscoverResult(
supportedVersions = listOf("2026-07-28"),
capabilities = ServerCapabilities(),
ttlMs = -1,
)
}
}
@Test
fun `unsupported version data should round trip without losing versions`() {
val data = UnsupportedProtocolVersionData(
supported = listOf("2026-07-28", "2025-11-25"),
requested = "2099-01-01",
)
val encoded = McpJson.encodeToString(data)
assertEquals(data, McpJson.decodeFromString(encoded))
assertEquals(-32022, RPCError.ErrorCode.UNSUPPORTED_PROTOCOL_VERSION)
}
}

View file

@ -2,20 +2,120 @@ package io.modelcontextprotocol.kotlin.sdk.types
import io.kotest.assertions.json.shouldEqualJson import io.kotest.assertions.json.shouldEqualJson
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization
import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import io.modelcontextprotocol.kotlin.test.utils.verifySerialization
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.double import kotlinx.serialization.json.double
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertIs import kotlin.test.assertIs
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertNull import kotlin.test.assertNull
import kotlin.test.assertTrue
class RequestTest { class RequestTest {
@OptIn(ExperimentalMcpApi::class)
@Suppress("DEPRECATION")
@Test
fun `should decode typed request metadata without discarding extensions`() {
val request = McpJson.decodeFromString<Request>(
"""
{
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "wire-client",
"version": "1.2.3"
},
"io.modelcontextprotocol/clientCapabilities": {
"sampling": {},
"com.example/custom-capability": {"enabled": true}
},
"io.modelcontextprotocol/logLevel": "warning",
"com.example/traceId": "trace-123"
}
}
}
""".trimIndent(),
)
val listTools = assertIs<ListToolsRequest>(request)
val meta = assertNotNull(listTools.params?.meta)
meta.protocolVersion shouldBe "2026-07-28"
meta.clientInfo shouldBe Implementation(name = "wire-client", version = "1.2.3")
assertNotNull(meta.clientCapabilities?.sampling)
meta.logLevel shouldBe LoggingLevel.Warning
meta["com.example/traceId"]?.jsonPrimitive?.content shouldBe "trace-123"
assertNotNull(meta.json[RequestMetaKeys.CLIENT_CAPABILITIES]?.let { it as? JsonObject })
}
@OptIn(ExperimentalMcpApi::class)
@Test
fun `empty client capabilities should mean no optional capabilities`() {
val json = Json.parseToJsonElement(
"""{"${RequestMetaKeys.CLIENT_CAPABILITIES}": {}}""",
) as JsonObject
RequestMeta(json).clientCapabilities shouldBe ClientCapabilities()
}
@OptIn(ExperimentalMcpApi::class)
@Suppress("DEPRECATION")
@Test
fun `typed request metadata should reject malformed fields`() {
val malformedValues = listOf(
RequestMetaKeys.PROTOCOL_VERSION to "{}",
RequestMetaKeys.PROTOCOL_VERSION to "null",
RequestMetaKeys.CLIENT_INFO to "\"not-an-implementation\"",
RequestMetaKeys.CLIENT_INFO to "{\"name\":\"missing-version\"}",
RequestMetaKeys.CLIENT_INFO to "null",
RequestMetaKeys.CLIENT_CAPABILITIES to "\"not-capabilities\"",
RequestMetaKeys.CLIENT_CAPABILITIES to "[]",
RequestMetaKeys.CLIENT_CAPABILITIES to "null",
RequestMetaKeys.LOG_LEVEL to "\"verbose\"",
RequestMetaKeys.LOG_LEVEL to "7",
RequestMetaKeys.LOG_LEVEL to "null",
)
malformedValues.forEach { (key, value) ->
val json = Json.parseToJsonElement("""{"$key":$value}""") as JsonObject
val meta = RequestMeta(json)
val failure = assertFailsWith<SerializationException> {
when (key) {
RequestMetaKeys.PROTOCOL_VERSION -> meta.protocolVersion
RequestMetaKeys.CLIENT_INFO -> meta.clientInfo
RequestMetaKeys.CLIENT_CAPABILITIES -> meta.clientCapabilities
else -> meta.logLevel
}
}
assertTrue(failure.message.orEmpty().contains(key))
}
}
@OptIn(ExperimentalMcpApi::class)
@Test
fun `legacy requests should not gain request scoped metadata`() {
val request = ListToolsRequest()
McpJson.encodeToString<Request>(request) shouldEqualJson """
{
"method": "tools/list"
}
""".trimIndent()
assertNull(request.params?.meta?.protocolVersion)
}
@Test @Test
fun `should expose progress token from RequestMeta`() { fun `should expose progress token from RequestMeta`() {
val stringTokenMeta = RequestMeta(buildJsonObject { put("progressToken", "sync-1") }) val stringTokenMeta = RequestMeta(buildJsonObject { put("progressToken", "sync-1") })