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:
parent
0ab49f30be
commit
76c12274ef
8 changed files with 782 additions and 0 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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") })
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue