Sourced from org.jetbrains.kotlin.jvm's releases.
Kotlin 2.4.0
Changelog
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
... (truncated)
Sourced from org.jetbrains.kotlin.jvm's changelog.
2.4.0
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
KT-82434Light classes should prefer enum entries to propertiesKT-84200SLC: return type is not boxed for delegated methods with generic original methodKT-72451"CCE: class PsiPrimitiveType cannot be cast to class PsiClassType" with same-named enum class and typealiasAnalysis API. PSI
... (truncated)
add726c
Add ChangeLog for 2.4.0-RC269a4a81
[Wasm] Add binaryen per-file arguments8907c63
[KGP] Suppress duplicate compiler warning output when --warning-mode=all
is a...f9bef12
[KGP] Add failing test for duplicate compiler warning output via
Problems API7fe49c2
KT-86268: Use 2.4.0 toolchain for abiValidation with older CVs13d25e2
KT-86268: Regression test addedf7d1a76
[JKlib] Fix JAR deserializer configuration.5cec663
[JKlib] Wire up JvmBuiltInClassDescriptorFactoryef14611
Make the default stability of non-final classes Unknown
instead of Stabled76473a
[Gradle] Fix GeneralNativeIT.shouldAllowToOverrideDownloadUrl test
failuresSourced from org.jetbrains.kotlin.jvm's releases.
Kotlin 2.4.0
Changelog
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
... (truncated)
Sourced from org.jetbrains.kotlin.jvm's changelog.
2.4.0
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
KT-82434Light classes should prefer enum entries to propertiesKT-84200SLC: return type is not boxed for delegated methods with generic original methodKT-72451"CCE: class PsiPrimitiveType cannot be cast to class PsiClassType" with same-named enum class and typealiasAnalysis API. PSI
... (truncated)
add726c
Add ChangeLog for 2.4.0-RC269a4a81
[Wasm] Add binaryen per-file arguments8907c63
[KGP] Suppress duplicate compiler warning output when --warning-mode=all
is a...f9bef12
[KGP] Add failing test for duplicate compiler warning output via
Problems API7fe49c2
KT-86268: Use 2.4.0 toolchain for abiValidation with older CVs13d25e2
KT-86268: Regression test addedf7d1a76
[JKlib] Fix JAR deserializer configuration.5cec663
[JKlib] Wire up JvmBuiltInClassDescriptorFactoryef14611
Make the default stability of non-final classes Unknown
instead of Stabled76473a
[Gradle] Fix GeneralNativeIT.shouldAllowToOverrideDownloadUrl test
failuresSourced from org.jetbrains.kotlin.plugin.serialization's releases.
Kotlin 2.4.0
Changelog
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
... (truncated)
Sourced from org.jetbrains.kotlin.plugin.serialization's changelog.
2.4.0
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
KT-82434Light classes should prefer enum entries to propertiesKT-84200SLC: return type is not boxed for delegated methods with generic original methodKT-72451"CCE: class PsiPrimitiveType cannot be cast to class PsiClassType" with same-named enum class and typealiasAnalysis API. PSI
... (truncated)
add726c
Add ChangeLog for 2.4.0-RC269a4a81
[Wasm] Add binaryen per-file arguments8907c63
[KGP] Suppress duplicate compiler warning output when --warning-mode=all
is a...f9bef12
[KGP] Add failing test for duplicate compiler warning output via
Problems API7fe49c2
KT-86268: Use 2.4.0 toolchain for abiValidation with older CVs13d25e2
KT-86268: Regression test addedf7d1a76
[JKlib] Fix JAR deserializer configuration.5cec663
[JKlib] Wire up JvmBuiltInClassDescriptorFactoryef14611
Make the default stability of non-final classes Unknown
instead of Stabled76473a
[Gradle] Fix GeneralNativeIT.shouldAllowToOverrideDownloadUrl test
failuresSourced from io.ktor:ktor-bom's releases.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
Sourced from io.ktor:ktor-bom's changelog.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
5ba9d6f
Release 3.5.1 (#5719)a4f145d
KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when…
(#5701)2caf278
Fix failing :ktor-io:compileDarwinMainKotlinMetadata (#5716)257de90
KTOR-8766 Fix multiple invocations of close from DIe77a168
Fix cache misses with content negotiation 3x (#5714)ed22950
Make huge post test less hugef329069
Update dependency com.github.luben:zstd-jni to v1.5.7-116ecd072
Update dependency io.swagger.parser.v3:swagger-parser to v2.1.44a0534cd
Update dependency io.pebbletemplates:pebble to v4.1.28647e53
Update dependency com.typesafe:config to v1.4.9Sourced from io.ktor:ktor-bom's releases.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
Sourced from io.ktor:ktor-bom's changelog.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
5ba9d6f
Release 3.5.1 (#5719)a4f145d
KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when…
(#5701)2caf278
Fix failing :ktor-io:compileDarwinMainKotlinMetadata (#5716)257de90
KTOR-8766 Fix multiple invocations of close from DIe77a168
Fix cache misses with content negotiation 3x (#5714)ed22950
Make huge post test less hugef329069
Update dependency com.github.luben:zstd-jni to v1.5.7-116ecd072
Update dependency io.swagger.parser.v3:swagger-parser to v2.1.44a0534cd
Update dependency io.pebbletemplates:pebble to v4.1.28647e53
Update dependency com.typesafe:config to v1.4.9Sourced from io.ktor:ktor-bom's releases.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
Sourced from io.ktor:ktor-bom's changelog.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
5ba9d6f
Release 3.5.1 (#5719)a4f145d
KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when…
(#5701)2caf278
Fix failing :ktor-io:compileDarwinMainKotlinMetadata (#5716)257de90
KTOR-8766 Fix multiple invocations of close from DIe77a168
Fix cache misses with content negotiation 3x (#5714)ed22950
Make huge post test less hugef329069
Update dependency com.github.luben:zstd-jni to v1.5.7-116ecd072
Update dependency io.swagger.parser.v3:swagger-parser to v2.1.44a0534cd
Update dependency io.pebbletemplates:pebble to v4.1.28647e53
Update dependency com.typesafe:config to v1.4.9Sourced from io.ktor:ktor-bom's releases.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
Sourced from io.ktor:ktor-bom's changelog.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
5ba9d6f
Release 3.5.1 (#5719)a4f145d
KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when…
(#5701)2caf278
Fix failing :ktor-io:compileDarwinMainKotlinMetadata (#5716)257de90
KTOR-8766 Fix multiple invocations of close from DIe77a168
Fix cache misses with content negotiation 3x (#5714)ed22950
Make huge post test less hugef329069
Update dependency com.github.luben:zstd-jni to v1.5.7-116ecd072
Update dependency io.swagger.parser.v3:swagger-parser to v2.1.44a0534cd
Update dependency io.pebbletemplates:pebble to v4.1.28647e53
Update dependency com.typesafe:config to v1.4.9Sourced from io.ktor:ktor-bom's releases.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
Sourced from io.ktor:ktor-bom's changelog.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
5ba9d6f
Release 3.5.1 (#5719)a4f145d
KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when…
(#5701)2caf278
Fix failing :ktor-io:compileDarwinMainKotlinMetadata (#5716)257de90
KTOR-8766 Fix multiple invocations of close from DIe77a168
Fix cache misses with content negotiation 3x (#5714)ed22950
Make huge post test less hugef329069
Update dependency com.github.luben:zstd-jni to v1.5.7-116ecd072
Update dependency io.swagger.parser.v3:swagger-parser to v2.1.44a0534cd
Update dependency io.pebbletemplates:pebble to v4.1.28647e53
Update dependency com.typesafe:config to v1.4.9Sourced from org.jetbrains.kotlin.jvm's releases.
Kotlin 2.4.0
Changelog
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
... (truncated)
Sourced from org.jetbrains.kotlin.jvm's changelog.
2.4.0
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
KT-82434Light classes should prefer enum entries to propertiesKT-84200SLC: return type is not boxed for delegated methods with generic original methodKT-72451"CCE: class PsiPrimitiveType cannot be cast to class PsiClassType" with same-named enum class and typealiasAnalysis API. PSI
... (truncated)
add726c
Add ChangeLog for 2.4.0-RC269a4a81
[Wasm] Add binaryen per-file arguments8907c63
[KGP] Suppress duplicate compiler warning output when --warning-mode=all
is a...f9bef12
[KGP] Add failing test for duplicate compiler warning output via
Problems API7fe49c2
KT-86268: Use 2.4.0 toolchain for abiValidation with older CVs13d25e2
KT-86268: Regression test addedf7d1a76
[JKlib] Fix JAR deserializer configuration.5cec663
[JKlib] Wire up JvmBuiltInClassDescriptorFactoryef14611
Make the default stability of non-final classes Unknown
instead of Stabled76473a
[Gradle] Fix GeneralNativeIT.shouldAllowToOverrideDownloadUrl test
failuresSourced from org.jetbrains.kotlin.plugin.serialization's releases.
Kotlin 2.4.0
Changelog
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
... (truncated)
Sourced from org.jetbrains.kotlin.plugin.serialization's changelog.
2.4.0
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
KT-82434Light classes should prefer enum entries to propertiesKT-84200SLC: return type is not boxed for delegated methods with generic original methodKT-72451"CCE: class PsiPrimitiveType cannot be cast to class PsiClassType" with same-named enum class and typealiasAnalysis API. PSI
... (truncated)
add726c
Add ChangeLog for 2.4.0-RC269a4a81
[Wasm] Add binaryen per-file arguments8907c63
[KGP] Suppress duplicate compiler warning output when --warning-mode=all
is a...f9bef12
[KGP] Add failing test for duplicate compiler warning output via
Problems API7fe49c2
KT-86268: Use 2.4.0 toolchain for abiValidation with older CVs13d25e2
KT-86268: Regression test addedf7d1a76
[JKlib] Fix JAR deserializer configuration.5cec663
[JKlib] Wire up JvmBuiltInClassDescriptorFactoryef14611
Make the default stability of non-final classes Unknown
instead of Stabled76473a
[Gradle] Fix GeneralNativeIT.shouldAllowToOverrideDownloadUrl test
failuresSourced from org.jetbrains.kotlin.jvm's releases.
Kotlin 2.4.0
Changelog
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
... (truncated)
Sourced from org.jetbrains.kotlin.jvm's changelog.
2.4.0
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
KT-82434Light classes should prefer enum entries to propertiesKT-84200SLC: return type is not boxed for delegated methods with generic original methodKT-72451"CCE: class PsiPrimitiveType cannot be cast to class PsiClassType" with same-named enum class and typealiasAnalysis API. PSI
... (truncated)
add726c
Add ChangeLog for 2.4.0-RC269a4a81
[Wasm] Add binaryen per-file arguments8907c63
[KGP] Suppress duplicate compiler warning output when --warning-mode=all
is a...f9bef12
[KGP] Add failing test for duplicate compiler warning output via
Problems API7fe49c2
KT-86268: Use 2.4.0 toolchain for abiValidation with older CVs13d25e2
KT-86268: Regression test addedf7d1a76
[JKlib] Fix JAR deserializer configuration.5cec663
[JKlib] Wire up JvmBuiltInClassDescriptorFactoryef14611
Make the default stability of non-final classes Unknown
instead of Stabled76473a
[Gradle] Fix GeneralNativeIT.shouldAllowToOverrideDownloadUrl test
failuresSourced from org.jetbrains.kotlin.plugin.serialization's releases.
Kotlin 2.4.0
Changelog
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
... (truncated)
Sourced from org.jetbrains.kotlin.plugin.serialization's changelog.
2.4.0
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
KT-82434Light classes should prefer enum entries to propertiesKT-84200SLC: return type is not boxed for delegated methods with generic original methodKT-72451"CCE: class PsiPrimitiveType cannot be cast to class PsiClassType" with same-named enum class and typealiasAnalysis API. PSI
... (truncated)
add726c
Add ChangeLog for 2.4.0-RC269a4a81
[Wasm] Add binaryen per-file arguments8907c63
[KGP] Suppress duplicate compiler warning output when --warning-mode=all
is a...f9bef12
[KGP] Add failing test for duplicate compiler warning output via
Problems API7fe49c2
KT-86268: Use 2.4.0 toolchain for abiValidation with older CVs13d25e2
KT-86268: Regression test addedf7d1a76
[JKlib] Fix JAR deserializer configuration.5cec663
[JKlib] Wire up JvmBuiltInClassDescriptorFactoryef14611
Make the default stability of non-final classes Unknown
instead of Stabled76473a
[Gradle] Fix GeneralNativeIT.shouldAllowToOverrideDownloadUrl test
failuresSourced from org.jetbrains.kotlin.jvm's releases.
Kotlin 2.4.0
Changelog
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
... (truncated)
Sourced from org.jetbrains.kotlin.jvm's changelog.
2.4.0
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
KT-82434Light classes should prefer enum entries to propertiesKT-84200SLC: return type is not boxed for delegated methods with generic original methodKT-72451"CCE: class PsiPrimitiveType cannot be cast to class PsiClassType" with same-named enum class and typealiasAnalysis API. PSI
... (truncated)
add726c
Add ChangeLog for 2.4.0-RC269a4a81
[Wasm] Add binaryen per-file arguments8907c63
[KGP] Suppress duplicate compiler warning output when --warning-mode=all
is a...f9bef12
[KGP] Add failing test for duplicate compiler warning output via
Problems API7fe49c2
KT-86268: Use 2.4.0 toolchain for abiValidation with older CVs13d25e2
KT-86268: Regression test addedf7d1a76
[JKlib] Fix JAR deserializer configuration.5cec663
[JKlib] Wire up JvmBuiltInClassDescriptorFactoryef14611
Make the default stability of non-final classes Unknown
instead of Stabled76473a
[Gradle] Fix GeneralNativeIT.shouldAllowToOverrideDownloadUrl test
failuresSourced from org.jetbrains.kotlin.plugin.serialization's releases.
Kotlin 2.4.0
Changelog
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
... (truncated)
Sourced from org.jetbrains.kotlin.plugin.serialization's changelog.
2.4.0
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
KT-82434Light classes should prefer enum entries to propertiesKT-84200SLC: return type is not boxed for delegated methods with generic original methodKT-72451"CCE: class PsiPrimitiveType cannot be cast to class PsiClassType" with same-named enum class and typealiasAnalysis API. PSI
... (truncated)
add726c
Add ChangeLog for 2.4.0-RC269a4a81
[Wasm] Add binaryen per-file arguments8907c63
[KGP] Suppress duplicate compiler warning output when --warning-mode=all
is a...f9bef12
[KGP] Add failing test for duplicate compiler warning output via
Problems API7fe49c2
KT-86268: Use 2.4.0 toolchain for abiValidation with older CVs13d25e2
KT-86268: Regression test addedf7d1a76
[JKlib] Fix JAR deserializer configuration.5cec663
[JKlib] Wire up JvmBuiltInClassDescriptorFactoryef14611
Make the default stability of non-final classes Unknown
instead of Stabled76473a
[Gradle] Fix GeneralNativeIT.shouldAllowToOverrideDownloadUrl test
failuresSourced from io.ktor:ktor-client-apache5's releases.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
Sourced from io.ktor:ktor-client-apache5's changelog.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
5ba9d6f
Release 3.5.1 (#5719)a4f145d
KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when…
(#5701)2caf278
Fix failing :ktor-io:compileDarwinMainKotlinMetadata (#5716)257de90
KTOR-8766 Fix multiple invocations of close from DIe77a168
Fix cache misses with content negotiation 3x (#5714)ed22950
Make huge post test less hugef329069
Update dependency com.github.luben:zstd-jni to v1.5.7-116ecd072
Update dependency io.swagger.parser.v3:swagger-parser to v2.1.44a0534cd
Update dependency io.pebbletemplates:pebble to v4.1.28647e53
Update dependency com.typesafe:config to v1.4.9Sourced from io.ktor:ktor-client-auth's releases.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
Sourced from io.ktor:ktor-client-auth's changelog.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
5ba9d6f
Release 3.5.1 (#5719)a4f145d
KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when…
(#5701)2caf278
Fix failing :ktor-io:compileDarwinMainKotlinMetadata (#5716)257de90
KTOR-8766 Fix multiple invocations of close from DIe77a168
Fix cache misses with content negotiation 3x (#5714)ed22950
Make huge post test less hugef329069
Update dependency com.github.luben:zstd-jni to v1.5.7-116ecd072
Update dependency io.swagger.parser.v3:swagger-parser to v2.1.44a0534cd
Update dependency io.pebbletemplates:pebble to v4.1.28647e53
Update dependency com.typesafe:config to v1.4.9Sourced from io.ktor:ktor-client-core's releases.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
Sourced from io.ktor:ktor-client-core's changelog.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
5ba9d6f
Release 3.5.1 (#5719)a4f145d
KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when…
(#5701)2caf278
Fix failing :ktor-io:compileDarwinMainKotlinMetadata (#5716)257de90
KTOR-8766 Fix multiple invocations of close from DIe77a168
Fix cache misses with content negotiation 3x (#5714)ed22950
Make huge post test less hugef329069
Update dependency com.github.luben:zstd-jni to v1.5.7-116ecd072
Update dependency io.swagger.parser.v3:swagger-parser to v2.1.44a0534cd
Update dependency io.pebbletemplates:pebble to v4.1.28647e53
Update dependency com.typesafe:config to v1.4.9Sourced from io.ktor:ktor-client-logging's releases.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated to the RawSource
- KTOR-8870 File.readChannel: do not close a non-opened file and ignore closing exceptions
- KTOR-9460 Curl: Can't build shared library with Ktor 3.4.2
- KTOR-9607 Crash on Android 7: NoSuchMethodError getInstanceStrong() since 3.5.0
- KTOR-9595 CIO: The engine imports
node:netstatically and breaks wasmJsBrowser webpack build- KTOR-9615 Apache5: The underlying request is not aborted when coroutine Job is cancelled
- KTOR-8443 RoutingBuilder.contentType does not use parameters when matching
- KTOR-9425 SessionTransportTransformerEncrypt init block uses wrong IV size for AES-256 (regression of KTOR-661)
- KTOR-9621 RateLimit plugin is bypassed when the nested authenticate block rejects the request
- KTOR-8766 DI: The close method is called twice on cleanup of AutoCloseable
- KTOR-8695 GMTDate() being offset by 369 years on Windows
- KTOR-9589 Duplicate PROPERTY_SETTER target in
@InternalAPIannotation
... (truncated)
Sourced from io.ktor:ktor-client-logging's changelog.
3.5.1
Published 25 June 2026
Improvements
- KTOR-9492 OpenAPI plugin: handle requireXxx functions in compiler plugin
- KTOR-9569 Add more details for KDoc of BearerAuthConfig.sendWithoutRequest
- KTOR-9571 Add KDoc for BearerAuthConfig.realm
- KTOR-9570 Add Kdoc for BearerAuthConfig.refreshTokens
Bugfixes
- KTOR-9646 Fix Kotlin 2.4.0 compiler plugin breaking changes
- KTOR-9585 OpenAPI: JsonSchema title is truncated when it contains a dot
- KTOR-9591 OpenAPI:
@JsonClassDiscriminatoron sealed types is ignored when generating OpenAPI discriminator schema- KTOR-9597 OpenAPI: Schema name collisions because
@SerialNameof sealed subtypes is used ascomponents.schemaskey- KTOR-9601 OpenAPI: nullable
@JvmInlinevalue class loses nullability in generated schema- KTOR-9620 Some issues related to digest authentication in 3.5.0
- KTOR-9623 Digest Auth: server must respond with one WWW-Authenticate header for each supported algorithm
- KTOR-9624 Digest Auth: "Unsupported charset in digest authentication header" for a charset name in lowercase
- KTOR-9630 Digest Auth: The plugin sends incorrect nonce when server responds with multiple WWW-Authenticate headers
- KTOR-9565 WebRTC: Peer stops receiving messages after a while on iOS
- KTOR-9610 WebRtcPeerConnection::close causes ClosedReceiveChannelException for other peer
- KTOR-9633 WebRTC statistics fetching race condition
- KTOR-9603 Structured concurrency is violated in WebRTC tests
- KTOR-9671 Darwin: Semicolons in URL path are sanitized
- KTOR-9639 Darwin: WebSocket client crashes the process when a PONG arrives after the session is closed
- KTOR-9575 Darwin: findCharset("UTF-16") maps to NSUTF16LittleEndianStringEncoding causing decoding failure for UTF-16 content with BOM
- KTOR-9661 HttpCache: InvalidCacheStateException is thrown on 304 when no cached entry matches varyKeys
- KTOR-9673 Caching always missing with Content Negotiation
- KTOR-9596 HttpTimeout: HttpRequestTimeoutException on any request with runTest and request timeout defined since 3.5.0
- KTOR-9632 HttpRequestBuilder.timeout call removes capabilities set in DefaultRequest
- KTOR-9252 ContentEncoding: Incomplete gzip response causes client/server to hang indefinitely
- KTOR-9670 Compression: The plugin ignores Accept-Encoding q=0 and most-specific matching
- KTOR-9614 Zstd Compression: "Destination buffer is too small" exception for a particular sequence of bytes
- KTOR-9636 CORS plugin drops OPTIONS preflight requests when allowSameOrigin is on
- KTOR-9659 CORS is skipped when the Origin header contains an IPv6 address
- KTOR-9574 ContentNegotiation: The charset of
Accept-Charsetheader is used for response deserialization- KTOR-9606 KotlinxSerializationConverter: it fails to deserialize an empty channel closed with a delay
- KTOR-9617 OutputStreamContent and WriterContent can exhaust Dispatchers.IO
- KTOR-9629 RawSourceChannel: coroutine cancellation is not propagated ... _Description has been truncated_ Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ae38db6..9e864e8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ collections-immutable = "0.5.0" coroutines = "1.11.0" kotest = "6.2.1" kotlinx-io = "0.9.1" -ktor = "3.4.3" +ktor = "3.5.1" logging = "8.0.4" mockk = "1.14.11" serialization = "1.11.0" From 4531a4c846ef54a5c9352888b4ab6a6541d33d9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:37:39 +0200 Subject: [PATCH 012/106] chore(deps): bump dev.detekt:detekt-gradle-plugin from 2.0.0-alpha.3 to 2.0.0-alpha.5 (#849) Bumps [dev.detekt:detekt-gradle-plugin](https://github.com/detekt/detekt) from 2.0.0-alpha.3 to 2.0.0-alpha.5. Release notes
Sourced from dev.detekt:detekt-gradle-plugin's releases.
v2.0.0-alpha.5
2.0.0-alpha.5 - 2026-06-17
This is a hotfix release for
2.0.0-alpha.4where we fixed 2 bugs that were preventing detekt from running correctly.Changelog
- fix: stop leaking ktlint-repackage as a transitive dependency - #9391
- fix: exclude gradle-public-api from published POM - #9408
- Stop accepting unsupported API or language versions passed to CLI - #9410
v2.0.0-alpha.4
2.0.0-alpha.4 - 2026-06-13
This is an alpha release of Detekt 2.0.0. Is built against Kotlin 2.4.0, Gradle 9.5.1, AGP 9.2.1 and is tested against JDK 25.
Notable Changes
- Support Kotlin 2.4.0 - #9218
- Add new rule
LeakingSessionBoundType- #9242- Apply the
indent_styleconfiguration to all the ktlint wrapper rules (previsouly this was applied only to theIndentationrule) - #9315Breaking Changes
Changelog
- fix: prevent ArrayPrimitive crash on unresolved types - #9364
- Update ktlint documentation links - #9354
- Fix PrintStackTrace nested call detection - #9352
- Fix unused import false positive for companion object member extension properties - #9349
- Accept punctuation after a link - #9341
- Fixes UnnecessaryFullyQualifiedName false positive with explicit import shadowing - #9329
- docs: fix KMP type resolution task names - #9316
- Stop publishing ktlint-repackage - #9314
- Stop exposing ktlint's StandardRule to detekt-rules-ktlint-wrapper consumers - #9313
- Include ktlint-repackage in ktlint-wrapper JAR - #9312
- Declare DGP compatibility with Gradle's configuration cache - #9311
- Forward languageVersionSettings to KotlinAnalysisApiEngine.compile - #9305
- docs: add ForbiddenImport configuration example - #9302
- Fix
UnnecessaryFullyQualifiedNameclashes with kotlin package name - #9289- Remove excludeImportStatements from Deprecation rule - #9287
- Improve repackaging of Analysis API dependencies - #9285
- MaxChainedCallsOnSameLine - Fix AA type leaks - #9261
- SuspendFunSwallowedCancellation - Fix AA type leaks - #9256
- feat: Add exhaustive option to OutdatedDocumentation rule, also improve error messages - #9255
- Excluding generated sources by default - #9251
... (truncated)
Commits
9c6ced6Stop accepting unsupported API or language versions passed to CLI (#9410)44526f4fix: exclude gradle-public-api from published POM (#9408)6bad2e8fix: stop leaking ktlint-repackage as a transitive dependency (#9391)fd5683eUpdate dependency path-to-regexp to v0.2.5 (#9397)7865b27fix: resolve npm security vulnerabilities in website and bots dependencies (#...bb582d5Unbreak snapshot publishing (#9394)69866ddUpdate caniuse browser definitions (#9393)bfd3b5dAdd Docs for 2.0.0-alpha.4 (#9388)60300b2Switch to vanniktech/gradle-maven-publish-plugin for publishing (#9357)63e0777Update gradle/actions digest to 3f131e8 (#9386)- Additional commits viewable in compare view
--------- Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: devcrocod --- config/detekt/detekt.yml | 1 - gradle/libs.versions.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 98fce0e..a82b404 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -416,7 +416,6 @@ potential-bugs: Deprecation: active: false aliases: ['DEPRECATION'] - excludeImportStatements: false DontDowncastCollectionTypes: active: false DoubleMutabilityForCollection: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9e864e8..592fa25 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # plugins version atomicfu = "0.33.0" binaryCompatibilityValidatorPlugin = "0.18.1" -detekt = "2.0.0-alpha.3" +detekt = "2.0.0-alpha.5" dokka = "2.2.0" kotlin = "2.3.21" kover = "0.9.8" From 420b64d1bb2d35e86adf544499e6748ee314ec72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:16:58 +0200 Subject: [PATCH 013/106] chore(deps): bump the kotlin group across 1 directory with 2 updates (#834) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the kotlin group with 2 updates in the / directory: [org.jetbrains.kotlin:kotlin-gradle-plugin](https://github.com/JetBrains/kotlin) and [org.jetbrains.kotlin:kotlin-serialization](https://github.com/JetBrains/kotlin). Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 2.3.21 to 2.4.0 Release notes
Sourced from org.jetbrains.kotlin:kotlin-gradle-plugin's releases.
Kotlin 2.4.0
Changelog
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
... (truncated)
Changelog
Sourced from org.jetbrains.kotlin:kotlin-gradle-plugin's changelog.
2.4.0
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
KT-82434Light classes should prefer enum entries to propertiesKT-84200SLC: return type is not boxed for delegated methods with generic original methodKT-72451"CCE: class PsiPrimitiveType cannot be cast to class PsiClassType" with same-named enum class and typealiasAnalysis API. PSI
... (truncated)
Commits
add726cAdd ChangeLog for 2.4.0-RC269a4a81[Wasm] Add binaryen per-file arguments8907c63[KGP] Suppress duplicate compiler warning output when --warning-mode=all is a...f9bef12[KGP] Add failing test for duplicate compiler warning output via Problems API7fe49c2KT-86268: Use 2.4.0 toolchain for abiValidation with older CVs13d25e2KT-86268: Regression test addedf7d1a76[JKlib] Fix JAR deserializer configuration.5cec663[JKlib] Wire up JvmBuiltInClassDescriptorFactoryef14611Make the default stability of non-final classesUnknowninstead ofStabled76473a[Gradle] Fix GeneralNativeIT.shouldAllowToOverrideDownloadUrl test failures- Additional commits viewable in compare view
Updates `org.jetbrains.kotlin:kotlin-serialization` from 2.3.21 to 2.4.0Release notes
Sourced from org.jetbrains.kotlin:kotlin-serialization's releases.
Kotlin 2.4.0
Changelog
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
... (truncated)
Changelog
Sourced from org.jetbrains.kotlin:kotlin-serialization's changelog.
2.4.0
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
KT-82434Light classes should prefer enum entries to propertiesKT-84200SLC: return type is not boxed for delegated methods with generic original methodKT-72451"CCE: class PsiPrimitiveType cannot be cast to class PsiClassType" with same-named enum class and typealiasAnalysis API. PSI
... (truncated)
Commits
add726cAdd ChangeLog for 2.4.0-RC269a4a81[Wasm] Add binaryen per-file arguments8907c63[KGP] Suppress duplicate compiler warning output when --warning-mode=all is a...f9bef12[KGP] Add failing test for duplicate compiler warning output via Problems API7fe49c2KT-86268: Use 2.4.0 toolchain for abiValidation with older CVs13d25e2KT-86268: Regression test addedf7d1a76[JKlib] Fix JAR deserializer configuration.5cec663[JKlib] Wire up JvmBuiltInClassDescriptorFactoryef14611Make the default stability of non-final classesUnknowninstead ofStabled76473a[Gradle] Fix GeneralNativeIT.shouldAllowToOverrideDownloadUrl test failures- Additional commits viewable in compare view
Updates `org.jetbrains.kotlin:kotlin-serialization` from 2.3.21 to 2.4.0Release notes
Sourced from org.jetbrains.kotlin:kotlin-serialization's releases.
Kotlin 2.4.0
Changelog
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
... (truncated)
Changelog
Sourced from org.jetbrains.kotlin:kotlin-serialization's changelog.
2.4.0
Analysis API
KT-83867OVERLOAD_RESOLUTION_AMBIGUITY false positive with assertEquals in IJ repoKT-83723[Analysis API] Enable experimental KDoc resolver by defaultKT-83388Analysis API: properly support KMP in KotlinPackageProviderKT-65683Analysis API: Dangling file session creation causes acomputeIfAbsentcontract violationAnalysis API. Code Compilation
KT-78946Evaluation of variable with local class in type parameter leads to InventNamesForLocalClasses exceptionAnalysis API. FIR
KT-68260K2 AA: InvalidFirElementTypeException “For CALLABLE_REFERENCE_EXPRESSION with text::lam1, unexpected element of type: no element found” with illegal callable reference callKT-70896AA: False positive deprecation warning with override of built-in method in JDK mapped classKT-84711K2 IDE sometimes loses FIR plugin-generated declarations after file changesKT-84625Analysis API: collectDesignationPath fails for nested classes inside plugin-generated top-level classesKT-84596Improve K2 Jooq completion performanceKT-84525KaValueParameterSymbol#getHasSynthesizedName returns false for FirDeclarationOrigin.SubstitutionOverride.DeclarationSiteKT-83546Kotlin analysis reach ClsCustomNavigationPolicyKT-84259Move CommonDefaultImportsProvider to the frontend independent moduleKT-82945Analysis API: KotlinIllegalArgumentExceptionWithAttachments: Expected FirResolvedTypeRef with ConeKotlinType but was FirUserTypeRefImplKT-71135AA: exception from sealed inheritors checker whenanalyzeCopyAnalysis API. Infrastructure
Fixes
KT-83905Analysis API: Improve UX with test dataKT-84913Extract compiler classes used by the PSI & Analysis API to a separate moduleKT-64986Analysis API: Implement Analysis API tests for different KMP PlatformsKT-84776The test data manager misses the redundancy check in the update modeKT-84962The test data manager misses -ea flagKT-84388Preserve the EOF status in the test data manager to avoid extra changesKT-84362Analysis API tests produce many warnings due to "not yet loaded registry"KT-84279Test Data Manager fails on a clean buildKT-83913Exclude compiler-based Analysis API tests from Git trackingKT-80379Extract per-module test generators for AA testsKT-84120Move CLI modules out of kotlin-compiler-fe10-for-ideKT-83200Track external dependencies of the Analysis API modulesAnalysis API. Light Classes
KT-82434Light classes should prefer enum entries to propertiesKT-84200SLC: return type is not boxed for delegated methods with generic original methodKT-72451"CCE: class PsiPrimitiveType cannot be cast to class PsiClassType" with same-named enum class and typealiasAnalysis API. PSI
... (truncated)
Commits
add726cAdd ChangeLog for 2.4.0-RC269a4a81[Wasm] Add binaryen per-file arguments8907c63[KGP] Suppress duplicate compiler warning output when --warning-mode=all is a...f9bef12[KGP] Add failing test for duplicate compiler warning output via Problems API7fe49c2KT-86268: Use 2.4.0 toolchain for abiValidation with older CVs13d25e2KT-86268: Regression test addedf7d1a76[JKlib] Fix JAR deserializer configuration.5cec663[JKlib] Wire up JvmBuiltInClassDescriptorFactoryef14611Make the default stability of non-final classesUnknowninstead ofStabled76473a[Gradle] Fix GeneralNativeIT.shouldAllowToOverrideDownloadUrl test failures- Additional commits viewable in compare view
--------- Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: devcrocod --- README.md | 2 +- gradle/libs.versions.toml | 2 +- kotlin-sdk-core/api/kotlin-sdk-core.api | 32 ++++++++++++------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 58a2cad..aefadcf 100644 --- a/README.md +++ b/README.md @@ -729,7 +729,7 @@ val client = Client( ) client.setRequestHandler (Method.Defined.SamplingCreateMessage) { request, _ -> - val content = request.messages.lastOrNull()?.content + val content = request.messages.lastOrNull()?.content?.firstOrNull() val prompt = if (content is TextContent) content.text else "your topic" CreateMessageResult( model = "gpt-4o-mini", diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 592fa25..e7f4683 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ atomicfu = "0.33.0" binaryCompatibilityValidatorPlugin = "0.18.1" detekt = "2.0.0-alpha.5" dokka = "2.2.0" -kotlin = "2.3.21" +kotlin = "2.4.0" kover = "0.9.8" ktlint = "14.2.0" knit = "0.5.1" diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index cdee342..1935e9a 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -415,7 +415,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequestBuild public final fun arguments (Lkotlin/jvm/functions/Function1;)V public final fun arguments (Lkotlinx/serialization/json/JsonObject;)V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun getName ()Ljava/lang/String; public final fun setName (Ljava/lang/String;)V } @@ -954,7 +954,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CompleteRequestBuild public fun ()V public final fun argument (Ljava/lang/String;Ljava/lang/String;)V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/CompleteRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun context (Ljava/util/Map;)V public final fun context (Lkotlin/jvm/functions/Function1;)V public final fun ref (Lio/modelcontextprotocol/kotlin/sdk/types/Reference;)V @@ -1188,7 +1188,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun getContext ()Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext; public final fun getMaxTokens ()Ljava/lang/Integer; public final fun getStopSequences ()Ljava/util/List; @@ -1455,7 +1455,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequest$Compan public final class io/modelcontextprotocol/kotlin/sdk/types/ElicitRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun getMessage ()Ljava/lang/String; public final fun requestedSchema (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitRequestParams$RequestedSchema;)V public final fun requestedSchema (Lkotlin/jvm/functions/Function1;)V @@ -1833,7 +1833,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest$Com public final class io/modelcontextprotocol/kotlin/sdk/types/GetPromptRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/GetPromptRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun getArguments ()Ljava/util/Map; public final fun getName ()Ljava/lang/String; public final fun setArguments (Ljava/util/Map;)V @@ -2278,7 +2278,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeRequest$Co public final class io/modelcontextprotocol/kotlin/sdk/types/InitializeRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/InitializeRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun capabilities (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;)V public final fun capabilities (Lkotlin/jvm/functions/Function1;)V public final fun getProtocolVersion ()Ljava/lang/String; @@ -2668,7 +2668,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest$C public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListPromptsRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; } public final class io/modelcontextprotocol/kotlin/sdk/types/ListPromptsResult : io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { @@ -2739,7 +2739,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplate public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; } public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourceTemplatesResult : io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { @@ -2810,7 +2810,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListResourcesRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; } public final class io/modelcontextprotocol/kotlin/sdk/types/ListResourcesResult : io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { @@ -2880,7 +2880,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest$Com public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListRootsRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; } public final class io/modelcontextprotocol/kotlin/sdk/types/ListRootsResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult { @@ -3014,7 +3014,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest$Com public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/PaginatedRequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ListToolsRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; } public final class io/modelcontextprotocol/kotlin/sdk/types/ListToolsResult : io/modelcontextprotocol/kotlin/sdk/types/PaginatedResult, io/modelcontextprotocol/kotlin/sdk/types/ServerResult { @@ -3440,7 +3440,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequest$Companio public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/PingRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; } public final class io/modelcontextprotocol/kotlin/sdk/types/PingRequest_dslKt { @@ -3791,7 +3791,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest$ public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun getUri ()Ljava/lang/String; public final fun setUri (Ljava/lang/String;)V } @@ -4754,7 +4754,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest$Comp public final class io/modelcontextprotocol/kotlin/sdk/types/SetLevelRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/SetLevelRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun getLoggingLevel ()Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel; public final fun setLoggingLevel (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;)V } @@ -4920,7 +4920,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest$Com public final class io/modelcontextprotocol/kotlin/sdk/types/SubscribeRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun getUri ()Ljava/lang/String; public final fun setUri (Ljava/lang/String;)V } @@ -5688,7 +5688,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest$C public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestBuilder : io/modelcontextprotocol/kotlin/sdk/types/RequestBuilder { public fun ()V public fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest; - public synthetic fun build$kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; + public synthetic fun build$io_modelcontextprotocol_kotlin_sdk_core ()Lio/modelcontextprotocol/kotlin/sdk/types/Request; public final fun getUri ()Ljava/lang/String; public final fun setUri (Ljava/lang/String;)V } From 19091d35284316f69a0f7505ddb27e5cabf57186 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:13:30 +0200 Subject: [PATCH 014/106] chore(deps): bump org.jetbrains.kotlinx:kotlinx-collections-immutable from 0.5.0 to 0.5.1 in the kotlinx group (#873) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the kotlinx group with 1 update: [org.jetbrains.kotlinx:kotlinx-collections-immutable](https://github.com/Kotlin/kotlinx.collections.immutable). Updates `org.jetbrains.kotlinx:kotlinx-collections-immutable` from 0.5.0 to 0.5.1 Release notes
Sourced from org.jetbrains.kotlinx:kotlinx-collections-immutable's releases.
v0.5.1
- Fixed
PersistentOrderedMapBuilderreturning a previously built map frombuild()after updating an entry value throughMutableMap.MutableEntry.setValue#253, #274- Fixed an assertion failure in
PersistentOrderedSetBuilder.build()after interleavedbuildandremove/removeAllcalls #251- Provided a JPMS module descriptor for the JVM artifact — module
kotlinx.collections.immutablecompiled intoMETA-INF/versions/9, the artifact stays Java 8-compatible #268Full changelog: https://github.com/Kotlin/kotlinx.collections.immutable/compare/v0.5.0...v0.5.1
Changelog
Sourced from org.jetbrains.kotlinx:kotlinx-collections-immutable's changelog.
0.5.1
- Fixed
PersistentOrderedMapBuilderreturning a previously built map frombuild()after updating an entry value throughMutableMap.MutableEntry.setValue#253, #274- Fixed an assertion failure in
PersistentOrderedSetBuilder.build()after interleavedbuildandremove/removeAllcalls #251- Provided a JPMS module descriptor for the JVM artifact — module
kotlinx.collections.immutablecompiled intoMETA-INF/versions/9, the artifact stays Java 8-compatible #268Commits
ad741d1Release v0.5.1cb4cf67Applied artifacts validator plugin (#271)e9ca796Restore the direct links write in the ordered map builder entry (#274)6822eedFix the pull request exclusion in the VCS trigger branch filter (#276)8201f28Surface pull request branches on CI for manually started builds (#273)f3dec4fFix ordered set builder cache after removeAll (#251)96bf012Provide a JPMS module descriptor for the JVM artifact (#268)81382a1Migrate ABI validation to the Kotlin Gradle plugin (#264)694d34bInvalidate ordered map builder after entry value updates (#253)1d6fda3Fix the method-rename examples in the 0.5.0 changelog entry (#266)- See full diff in compare view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---Signed-off-by: dependabot[bot]Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot showignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e7f4683..0d9b5de 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ netty = "4.2.15.Final" # libraries version awaitility = "4.3.0" -collections-immutable = "0.5.0" +collections-immutable = "0.5.1" coroutines = "1.11.0" kotest = "6.2.1" kotlinx-io = "0.9.1" From ea3940ce477c9672b43447b9aedaa9fd98612df5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:39:14 +0200 Subject: [PATCH 015/106] chore(deps): bump io.modelcontextprotocol:kotlin-sdk-client from 0.13.0 to 0.14.0 in /samples/kotlin-mcp-client (#875) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [io.modelcontextprotocol:kotlin-sdk-client](https://github.com/modelcontextprotocol/kotlin-sdk) from 0.13.0 to 0.14.0. Commits
fc7916frelease: 0.14.0 (#867)57e546dfeat: SEP-1686 Tasks (#844)35da063fix: bound request body size in SSE and Streamable HTTP transports (#839)bbf77bachore(deps): bump@types/nodefrom 26.0.0 to 26.0.1 in /integration-test/src/...d2b1509chore(deps): bump the other-dependencies group across 5 directories with 5 up...0221b8dfix(client): close stdio read sources during shutdown (#798)19db4b0fix(client): gate completion/complete on the completions capability (#846)64cf29fchore(deps): bump gradle-wrapper from 9.6.0 to 9.6.1 (#856)cd91652fix: validate Origin against localhost by default in DNS rebinding protection...4103b87chore(deps): bump org.jetbrains.kotlinx:kotlinx-io-core from 0.9.0 to 0.9.1 i...- Additional commits viewable in compare view
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index b5167d9..fab3459 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -2,7 +2,7 @@ anthropic = "2.45.0" kotlin = "2.4.0" ktor = "3.5.1" -mcp-kotlin = "0.13.0" +mcp-kotlin = "0.14.0" shadow = "9.4.3" slf4j = "2.0.18" From 04e6d56fd6c5df0dea86cea303b121f3dd3d7ef0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:28:48 +0200 Subject: [PATCH 016/106] chore(deps): bump mcp-kotlin from 0.13.0 to 0.14.0 in /samples/kotlin-mcp-server (#876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.13.0 to 0.14.0. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.13.0 to 0.14.0 Commits
fc7916frelease: 0.14.0 (#867)57e546dfeat: SEP-1686 Tasks (#844)35da063fix: bound request body size in SSE and Streamable HTTP transports (#839)bbf77bachore(deps): bump@types/nodefrom 26.0.0 to 26.0.1 in /integration-test/src/...d2b1509chore(deps): bump the other-dependencies group across 5 directories with 5 up...0221b8dfix(client): close stdio read sources during shutdown (#798)19db4b0fix(client): gate completion/complete on the completions capability (#846)64cf29fchore(deps): bump gradle-wrapper from 9.6.0 to 9.6.1 (#856)cd91652fix: validate Origin against localhost by default in DNS rebinding protection...4103b87chore(deps): bump org.jetbrains.kotlinx:kotlinx-io-core from 0.9.0 to 0.9.1 i...- Additional commits viewable in compare view
Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.13.0 to 0.14.0Commits
fc7916frelease: 0.14.0 (#867)57e546dfeat: SEP-1686 Tasks (#844)35da063fix: bound request body size in SSE and Streamable HTTP transports (#839)bbf77bachore(deps): bump@types/nodefrom 26.0.0 to 26.0.1 in /integration-test/src/...d2b1509chore(deps): bump the other-dependencies group across 5 directories with 5 up...0221b8dfix(client): close stdio read sources during shutdown (#798)19db4b0fix(client): gate completion/complete on the completions capability (#846)64cf29fchore(deps): bump gradle-wrapper from 9.6.0 to 9.6.1 (#856)cd91652fix: validate Origin against localhost by default in DNS rebinding protection...4103b87chore(deps): bump org.jetbrains.kotlinx:kotlinx-io-core from 0.9.0 to 0.9.1 i...- Additional commits viewable in compare view
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-server/gradle/libs.versions.toml b/samples/kotlin-mcp-server/gradle/libs.versions.toml index 5a6217e..6c6ff89 100644 --- a/samples/kotlin-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-server/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.4.0" ktor = "3.5.1" -mcp-kotlin = "0.13.0" +mcp-kotlin = "0.14.0" slf4j = "2.0.18" shadow = "9.4.3" From 14c848db417b4c884990af2ce69d1a8bac6e40a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:29:15 +0200 Subject: [PATCH 017/106] chore(deps): bump mcp-kotlin from 0.13.0 to 0.14.0 in /samples/kotlinlang-mcp-server (#877) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.13.0 to 0.14.0. Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.13.0 to 0.14.0 Commits
fc7916frelease: 0.14.0 (#867)57e546dfeat: SEP-1686 Tasks (#844)35da063fix: bound request body size in SSE and Streamable HTTP transports (#839)bbf77bachore(deps): bump@types/nodefrom 26.0.0 to 26.0.1 in /integration-test/src/...d2b1509chore(deps): bump the other-dependencies group across 5 directories with 5 up...0221b8dfix(client): close stdio read sources during shutdown (#798)19db4b0fix(client): gate completion/complete on the completions capability (#846)64cf29fchore(deps): bump gradle-wrapper from 9.6.0 to 9.6.1 (#856)cd91652fix: validate Origin against localhost by default in DNS rebinding protection...4103b87chore(deps): bump org.jetbrains.kotlinx:kotlinx-io-core from 0.9.0 to 0.9.1 i...- Additional commits viewable in compare view
Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.13.0 to 0.14.0Commits
fc7916frelease: 0.14.0 (#867)57e546dfeat: SEP-1686 Tasks (#844)35da063fix: bound request body size in SSE and Streamable HTTP transports (#839)bbf77bachore(deps): bump@types/nodefrom 26.0.0 to 26.0.1 in /integration-test/src/...d2b1509chore(deps): bump the other-dependencies group across 5 directories with 5 up...0221b8dfix(client): close stdio read sources during shutdown (#798)19db4b0fix(client): gate completion/complete on the completions capability (#846)64cf29fchore(deps): bump gradle-wrapper from 9.6.0 to 9.6.1 (#856)cd91652fix: validate Origin against localhost by default in DNS rebinding protection...4103b87chore(deps): bump org.jetbrains.kotlinx:kotlinx-io-core from 0.9.0 to 0.9.1 i...- Additional commits viewable in compare view
Updates `io.modelcontextprotocol:kotlin-sdk-testing` from 0.13.0 to 0.14.0Commits
fc7916frelease: 0.14.0 (#867)57e546dfeat: SEP-1686 Tasks (#844)35da063fix: bound request body size in SSE and Streamable HTTP transports (#839)bbf77bachore(deps): bump@types/nodefrom 26.0.0 to 26.0.1 in /integration-test/src/...d2b1509chore(deps): bump the other-dependencies group across 5 directories with 5 up...0221b8dfix(client): close stdio read sources during shutdown (#798)19db4b0fix(client): gate completion/complete on the completions capability (#846)64cf29fchore(deps): bump gradle-wrapper from 9.6.0 to 9.6.1 (#856)cd91652fix: validate Origin against localhost by default in DNS rebinding protection...4103b87chore(deps): bump org.jetbrains.kotlinx:kotlinx-io-core from 0.9.0 to 0.9.1 i...- Additional commits viewable in compare view
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlinlang-mcp-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml index c521eba..766b5ab 100644 --- a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.4.0" ktor = "3.5.1" -mcp-kotlin = "0.13.0" +mcp-kotlin = "0.14.0" serialization = "1.11.0" coroutines = "1.11.0" logback = "1.5.37" From 7ea602a5bf259379c027c315e08b94269db0340e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:29:38 +0200 Subject: [PATCH 018/106] chore(deps): bump mcp-kotlin from 0.13.0 to 0.14.0 in /samples/simple-streamable-server (#878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.13.0 to 0.14.0. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.13.0 to 0.14.0 Commits
fc7916frelease: 0.14.0 (#867)57e546dfeat: SEP-1686 Tasks (#844)35da063fix: bound request body size in SSE and Streamable HTTP transports (#839)bbf77bachore(deps): bump@types/nodefrom 26.0.0 to 26.0.1 in /integration-test/src/...d2b1509chore(deps): bump the other-dependencies group across 5 directories with 5 up...0221b8dfix(client): close stdio read sources during shutdown (#798)19db4b0fix(client): gate completion/complete on the completions capability (#846)64cf29fchore(deps): bump gradle-wrapper from 9.6.0 to 9.6.1 (#856)cd91652fix: validate Origin against localhost by default in DNS rebinding protection...4103b87chore(deps): bump org.jetbrains.kotlinx:kotlinx-io-core from 0.9.0 to 0.9.1 i...- Additional commits viewable in compare view
Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.13.0 to 0.14.0Commits
fc7916frelease: 0.14.0 (#867)57e546dfeat: SEP-1686 Tasks (#844)35da063fix: bound request body size in SSE and Streamable HTTP transports (#839)bbf77bachore(deps): bump@types/nodefrom 26.0.0 to 26.0.1 in /integration-test/src/...d2b1509chore(deps): bump the other-dependencies group across 5 directories with 5 up...0221b8dfix(client): close stdio read sources during shutdown (#798)19db4b0fix(client): gate completion/complete on the completions capability (#846)64cf29fchore(deps): bump gradle-wrapper from 9.6.0 to 9.6.1 (#856)cd91652fix: validate Origin against localhost by default in DNS rebinding protection...4103b87chore(deps): bump org.jetbrains.kotlinx:kotlinx-io-core from 0.9.0 to 0.9.1 i...- Additional commits viewable in compare view
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/simple-streamable-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index e3f8f6a..21db581 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin = "2.4.0" ktor = "3.5.1" junit = "6.1.1" -mcp-kotlin = "0.13.0" +mcp-kotlin = "0.14.0" slf4j = "2.0.18" shadow = "9.4.3" From 91f7390a95dc7265b1202ad5226aaa8fd1d348a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:09:53 +0200 Subject: [PATCH 019/106] chore(deps): bump the other-dependencies group across 5 directories with 2 updates (#882) Bumps the other-dependencies group with 2 updates in the /samples/kotlin-mcp-client directory: [com.anthropic:anthropic-java](https://github.com/anthropics/anthropic-sdk-java) and [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 1 update in the /samples/kotlin-mcp-server directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 1 update in the /samples/kotlinlang-mcp-server directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 1 update in the /samples/simple-streamable-server directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 1 update in the /samples/weather-stdio-server directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow). Updates `com.anthropic:anthropic-java` from 2.45.0 to 2.48.0 Release notes
Sourced from com.anthropic:anthropic-java's releases.
v2.48.0
2.48.0 (2026-07-02)
Full Changelog: v2.47.1...v2.48.0
Features
- api: add agent-memory-2026-07-22 beta header (6603879)
v2.47.1
2.47.1 (2026-07-01)
Full Changelog: v2.47.0...v2.47.1
Chores
- api: remove some nonfunctional types from the SDKs (2f92a48)
v2.47.0
2.47.0 (2026-06-30)
Full Changelog: v2.46.0...v2.47.0
Features
- api: add support for Managed Agents event delta streaming, agent overrides, reverse pagination, vault credential injection scoping, and agent and deployment webhook events (5eed04b)
v2.46.0
2.46.0 (2026-06-30)
Full Changelog: v2.45.0...v2.46.0
Features
- api: add support for claude-sonnet-5 (444f054)
Changelog
Sourced from com.anthropic:anthropic-java's changelog.
2.48.0 (2026-07-02)
Full Changelog: v2.47.1...v2.48.0
Features
- api: add agent-memory-2026-07-22 beta header (6603879)
2.47.1 (2026-07-01)
Full Changelog: v2.47.0...v2.47.1
Chores
- api: remove some nonfunctional types from the SDKs (2f92a48)
2.47.0 (2026-06-30)
Full Changelog: v2.46.0...v2.47.0
Features
- api: add support for Managed Agents event delta streaming, agent overrides, reverse pagination, vault credential injection scoping, and agent and deployment webhook events (5eed04b)
2.46.0 (2026-06-30)
Full Changelog: v2.45.0...v2.46.0
Features
- api: add support for claude-sonnet-5 (444f054)
Commits
db3e617release: 2.48.0a462333feat(api): add agent-memory-2026-07-22 beta header3c6c77brelease: 2.47.10d315e6chore(api): remove some nonfunctional types from the SDKs2093447release: 2.47.04bf9078feat(api): add support for Managed Agents event delta streaming, agent overri...1c9486bcodegen metadatafab7574release: 2.46.0f0b5297feat(api): add support for claude-sonnet-5- See full diff in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.4.3 to 9.5.1Release notes
Sourced from com.gradleup.shadow's releases.
9.5.1
Fixed
- Fix eager calls for
toolchainSpecin Kotlin DSL.
This fixes the regression when usingtoolchainwithouttargetCompatibilityin Kotlin DSL.9.5.0
Added
- Check
DuplicatesStrategyfor merging transformers. (#2026)
This will log warnings when an incompatibleDuplicatesStrategy(e.g.,EXCLUDE) is applied in Gradle configuration for built-inResourceTransformers.- Add
KotlinModuleMetadataTransformer. (#2073)- Add R8 as an opt-in
minimize { r8 { ... } }tool for shrinking the final shadowed JAR. (#2077)Changed
- Bump min Gradle requirement to 9.2.0. (#2057)
- Remove
afterEvaluatewhen adding variants. (#2056)- Deprecate
enableKotlinModuleRemappingforShadowJar. (#2073)
ApplyKotlinModuleMetadataTransformerexplicitly to support relocating inside Kotlin module metadata files.- Deprecate everything under
ShadowCopyAction. (#2083)Fixed
- Fix the conflicts when using
afterEvaluatewith other plugins. (#2055)Commits
c33612dPrepare version 9.5.1485e412Fix eager calls for toolchainSpec in Kotlin DSL (#2087)d062fa2Fix DuplicatesStrategyCheckerTest resource loading on Windows (#2085)e71a2c1Add description for shadow configuration (#2084)7ba43b0Prepare next development version6e00cb7Prepare version 9.5.0ca03909Deprecate everything under ShadowCopyAction (#2083)7d7da27Revert "Mark CONSTANT_TIME_FOR_ZIP_ENTRIES as const (#2074)"da18cc0Deprecate enableKotlinModuleRemapping for ShadowJar (#2073)ec6e55aReplace system properties with buildConfig consts (#2082)- Additional commits viewable in Date: Mon, 6 Jul 2026 14:39:33 +0200 Subject: [PATCH 020/106] chore(deps): bump mcp-kotlin from 0.13.0 to 0.14.0 in /samples/weather-stdio-server (#879) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `mcp-kotlin` from 0.13.0 to 0.14.0. Updates `io.modelcontextprotocol:kotlin-sdk-client` from 0.13.0 to 0.14.0
Commits
fc7916frelease: 0.14.0 (#867)57e546dfeat: SEP-1686 Tasks (#844)35da063fix: bound request body size in SSE and Streamable HTTP transports (#839)bbf77bachore(deps): bump@types/nodefrom 26.0.0 to 26.0.1 in /integration-test/src/...d2b1509chore(deps): bump the other-dependencies group across 5 directories with 5 up...0221b8dfix(client): close stdio read sources during shutdown (#798)19db4b0fix(client): gate completion/complete on the completions capability (#846)64cf29fchore(deps): bump gradle-wrapper from 9.6.0 to 9.6.1 (#856)cd91652fix: validate Origin against localhost by default in DNS rebinding protection...4103b87chore(deps): bump org.jetbrains.kotlinx:kotlinx-io-core from 0.9.0 to 0.9.1 i...- Additional commits viewable in compare view
Updates `io.modelcontextprotocol:kotlin-sdk-server` from 0.13.0 to 0.14.0Commits
fc7916frelease: 0.14.0 (#867)57e546dfeat: SEP-1686 Tasks (#844)35da063fix: bound request body size in SSE and Streamable HTTP transports (#839)bbf77bachore(deps): bump@types/nodefrom 26.0.0 to 26.0.1 in /integration-test/src/...d2b1509chore(deps): bump the other-dependencies group across 5 directories with 5 up...0221b8dfix(client): close stdio read sources during shutdown (#798)19db4b0fix(client): gate completion/complete on the completions capability (#846)64cf29fchore(deps): bump gradle-wrapper from 9.6.0 to 9.6.1 (#856)cd91652fix: validate Origin against localhost by default in DNS rebinding protection...4103b87chore(deps): bump org.jetbrains.kotlinx:kotlinx-io-core from 0.9.0 to 0.9.1 i...- Additional commits viewable in compare view
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/weather-stdio-server/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/weather-stdio-server/gradle/libs.versions.toml b/samples/weather-stdio-server/gradle/libs.versions.toml index ee81756..67fec97 100644 --- a/samples/weather-stdio-server/gradle/libs.versions.toml +++ b/samples/weather-stdio-server/gradle/libs.versions.toml @@ -3,7 +3,7 @@ coroutines = "1.11.0" kotlin = "2.4.0" ktor = "3.5.1" logging = "8.0.4" -mcp-kotlin = "0.13.0" +mcp-kotlin = "0.14.0" shadow = "9.5.1" slf4j = "2.0.18" From 905d4f975e415a1113922465ef8013d9eedfdb21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:00:40 +0000 Subject: [PATCH 021/106] chore(deps): bump the all-dependencies group across 1 directory with 2 updates (#880) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-dependencies group with 2 updates in the /integration-test/src/jvmTest/typescript directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [tsx](https://github.com/privatenumber/tsx). Updates `@types/node` from 26.0.1 to 26.1.0 Commits
- See full diff in compare view
Updates `tsx` from 4.22.4 to 4.23.0Release notes
Sourced from tsx's releases.
v4.23.0
4.23.0 (2026-07-03)
Bug Fixes
- avoid redundant filesystem probes during module resolution (257bbbb), closes privatenumber/tsx#809
Features
- add multi-scenario startup benchmark suite (c178197), closes privatenumber/tsx#809 #809 hi#signal privatenumber/tsx#145 #809
This release is also available on:
v4.22.5
4.22.5 (2026-07-02)
Bug Fixes
- isolate hook state per async module.register() registration (a305f36)
This release is also available on:
Commits
1dfad37docs: cite esbuild's extension-resolution model in node notes257bbbbfix: avoid redundant filesystem probes during module resolutionc178197feat: add multi-scenario startup benchmark suite51800acdocs: add Node internals knowledge base (notes/node)a305f36fix: isolate hook state per async module.register() registrationca501a9chore: upgrade skills-npm to v1.2.0596cd1ftest: cover __dirname, __filename, & require.cache in CJS TS file75d9bf0test: lock in lenient ESM for ambiguous and CJS-typed packages1472f3etest: cover ESM-syntax dependency with omitted "type" field- See full diff in compare view
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/jvmTest/typescript/package-lock.json | 16 ++++++++-------- .../src/jvmTest/typescript/package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index befd50e..4d0c500 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -15,9 +15,9 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^26.0.1", + "@types/node": "^26.1.0", "cors": "^2.8.6", - "tsx": "^4.22.4", + "tsx": "^4.23.0", "typescript": "^6.0.3" } }, @@ -579,9 +579,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.0.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", - "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", "dev": true, "license": "MIT", "dependencies": { @@ -1680,9 +1680,9 @@ } }, "node_modules/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", + "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index aa8d9db..bf81140 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -16,9 +16,9 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^26.0.1", + "@types/node": "^26.1.0", "cors": "^2.8.6", - "tsx": "^4.22.4", + "tsx": "^4.23.0", "typescript": "^6.0.3" } } From 7937390b4787f758c82c369541374d050fbdcd9b Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Thu, 9 Jul 2026 17:03:01 +0200 Subject: [PATCH 022/106] test: create the server session before connecting the client (#885) `ChannelTransportTest` connected both sides concurrently, which is racy. `ChannelTransport` starts delivering inbound messages when its read loop launches, which happens before `start()` marks the transport `Operational`. The two run on different dispatchers, so the client's `initialize` could reach the server read loop first and the session's `InitializeResult` was then rejected by the send state gate with `Transport is not ready`. Measured ~3 failures in 600 rounds. `createSession()` does not await the handshake, so creating the session first is enough to order the two. This also matches the usage shown in `createLinkedPair()`'s KDoc. Not addressed here: the transport itself should not deliver inbound messages before it is `Operational`. Fixing that needs a post-`Operational` hook on `AbstractClientTransport` that the read loop awaits. --- .../kotlin/sdk/testing/ChannelTransportTest.kt | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt index ea246db..4d798ce 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/testing/ChannelTransportTest.kt @@ -6,7 +6,6 @@ import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions -import io.modelcontextprotocol.kotlin.sdk.server.ServerSession import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult @@ -16,9 +15,6 @@ import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult import io.modelcontextprotocol.kotlin.sdk.types.Method import io.modelcontextprotocol.kotlin.sdk.types.Resource import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlin.test.Test @@ -43,18 +39,9 @@ class ChannelTransportTest { clientInfo = Implementation(name = "test client", version = "1.0"), ) - val serverSessionResult = CompletableDeferred () + val serverSession = server.createSession(serverTransport) + client.connect(clientTransport) - listOf( - launch { - client.connect(clientTransport) - }, - launch { - serverSessionResult.complete(server.createSession(serverTransport)) - }, - ).joinAll() - - val serverSession = serverSessionResult.await() serverSession.setRequestHandler (Method.Defined.Initialize) { _, _ -> InitializeResult( protocolVersion = LATEST_PROTOCOL_VERSION, From beb340006498a73f83205e431344c0ff70687984 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Wed, 15 Jul 2026 12:33:29 +0200 Subject: [PATCH 023/106] feat(core): concurrent inbound dispatch behind an initialization gate (#884) Make inbound message handling concurrent after the initialization handshake, so a slow or peer-awaiting handler no longer blocks other messages on the same connection fixes #176, #572 ## Motivation and Context Inbound messages were processed serially on the transport read loop. One slow or peer-awaiting request handler (sampling, elicitation, roots) blocked every later message on that connection, including the responses and `notifications/cancelled` those handlers depend on which could deadlock request/response flows. ## How Has This Been Tested? New and old tests ## Breaking Changes - after the handshake, inbound handlers may run concurrently (processing stays serial during initialization). There is no opt-out flag by design, concurrency is bounded by `maxConcurrentHandlers` / `maxInFlightHandlers`. - `ProtocolOptions` / `ClientOptions` / `ServerOptions` gain new parameters with defaults - `RequestHandlerExtra` is now a coroutine-context element and is no longer user-constructible ## Types of changes - [x] 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 --- .../kotlin/sdk/client/ClientTest.kt | 38 +- .../ProtocolConcurrencyIntegrationTest.kt | 306 ++++++++++ .../sdk/server/ClientConnectionLoggingTest.kt | 22 +- .../sdk/server/ServerSessionInitializeTest.kt | 2 + kotlin-sdk-client/api/kotlin-sdk-client.api | 4 +- .../kotlin/sdk/client/Client.kt | 10 +- kotlin-sdk-core/api/kotlin-sdk-core.api | 21 +- .../kotlin/sdk/shared/Protocol.kt | 569 +++++++++++++++--- .../kotlin/sdk/types/methods.kt | 9 + .../kotlin/sdk/types/serializers.kt | 9 +- .../sdk/shared/ProtocolConcurrencyTest.kt | 524 ++++++++++++++++ .../kotlin/sdk/shared/ProtocolOptionsTest.kt | 16 + .../ProtocolOutboundCancellationTest.kt | 167 +++++ .../kotlin/sdk/shared/ProtocolTest.kt | 197 ++++-- .../kotlin/sdk/shared/ProtocolTestFakes.kt | 112 ++++ .../sdk/shared/ProtocolConcurrencyJvmTest.kt | 114 ++++ kotlin-sdk-server/api/kotlin-sdk-server.api | 5 +- .../kotlin/sdk/server/Server.kt | 9 +- .../kotlin/sdk/server/ServerSession.kt | 8 + .../server/StreamableHttpServerTransport.kt | 191 +++++- .../StreamableHttpServerTransportTest.kt | 326 ++++++++++ 21 files changed, 2465 insertions(+), 194 deletions(-) create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/ProtocolConcurrencyIntegrationTest.kt create mode 100644 kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyTest.kt create mode 100644 kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptionsTest.kt create mode 100644 kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOutboundCancellationTest.kt create mode 100644 kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTestFakes.kt create mode 100644 kotlin-sdk-core/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyJvmTest.kt diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index 5dfcf61..54d25ee 100644 --- a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -24,6 +24,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION @@ -52,6 +53,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema import io.modelcontextprotocol.kotlin.sdk.types.UrlElicitationRequiredException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.joinAll @@ -71,6 +73,7 @@ import kotlin.test.assertIs import kotlin.test.assertNull import kotlin.test.assertTrue import kotlin.test.fail +import kotlin.time.Duration.Companion.seconds class ClientTest { @Test @@ -543,6 +546,15 @@ class ClientTest { val serverSession = serverSessionResult.await() + // Register the tap after createSession so it chains onto the session's subscription instead + // of pre-empting the handshake. + val cancelledOnWire = CompletableDeferred () + serverTransport.onMessage { message -> + if (message is JSONRPCNotification && message.method == Method.Defined.NotificationsCancelled.value) { + cancelledOnWire.complete(Unit) + } + } + serverSession.setRequestHandler (Method.Defined.ResourcesList) { _, _ -> // Simulate delay def.complete(Unit) @@ -568,6 +580,7 @@ class ClientTest { def.await() runCatching { job.cancel("Cancelled by test") } defCancel.await() + cancelledOnWire.await() defTimeOut.await() } @@ -606,17 +619,9 @@ class ClientTest { val serverSession = serverSessionResult.await() serverSession.setRequestHandler (Method.Defined.ResourcesList) { _, _ -> - // Simulate a delayed response - // Wait ~100ms unless canceled - try { - withTimeout(100L) { - // Just delay here, if timeout is 0 on the client side, this won't return in time - delay(100) - } - } catch (_: Exception) { - // If aborted, just rethrow or return early - } - ListResourcesResult(resources = emptyList()) + // Never respond: the handler runs on a real dispatcher while the 1 ms timeout runs on the + // virtual test clock, so a real-time delay could race it. Parking keeps the test deterministic. + awaitCancellation() } // Request with 1 msec timeout should fail immediately @@ -895,8 +900,8 @@ class ClientTest { ), ) - // Track notifications - var rootListChangedNotificationReceived = false + // The handler runs on Dispatchers.Default after concurrent dispatch is enabled; await the deferred. + val rootListChangedReceived = CompletableDeferred () val serverSessionResult = CompletableDeferred () @@ -915,16 +920,13 @@ class ClientTest { serverSession.setNotificationHandler ( Method.Defined.NotificationsRootsListChanged, ) { - rootListChangedNotificationReceived = true + rootListChangedReceived.complete(Unit) CompletableDeferred(Unit) } client.sendRootsListChanged() - assertTrue( - rootListChangedNotificationReceived, - "Notification should be sent when sendRootsListChanged is called", - ) + withTimeout(5.seconds) { rootListChangedReceived.await() } } @Test diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/ProtocolConcurrencyIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/ProtocolConcurrencyIntegrationTest.kt new file mode 100644 index 0000000..d8d64ec --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/ProtocolConcurrencyIntegrationTest.kt @@ -0,0 +1,306 @@ +package io.modelcontextprotocol.kotlin.sdk.integration + +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.server.ServerSession +import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport +import io.modelcontextprotocol.kotlin.sdk.shared.currentRequestHandlerExtra +import io.modelcontextprotocol.kotlin.sdk.testing.ChannelTransport +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest +import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import io.modelcontextprotocol.kotlin.sdk.types.Role +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicReference +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.time.Duration.Companion.seconds + +/** + * End-to-end tests for concurrent inbound dispatch enabled after the MCP handshake. + * + * Handlers run on a real [kotlinx.coroutines.Dispatchers.Default], so these use [runBlocking] with + * explicit [withTimeout] guards. A virtual test clock would false-fire timeouts against cross-thread + * real dispatchers. + */ +@OptIn(ExperimentalMcpApi::class) +class ProtocolConcurrencyIntegrationTest { + + private fun rootsClient(): Client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + options = ClientOptions( + capabilities = ClientCapabilities(roots = ClientCapabilities.Roots(listChanged = null)), + ), + ) + + private fun toolsServer(): Server = Server( + serverInfo = Implementation(name = "test server", version = "1.0"), + options = ServerOptions( + capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = null)), + ), + ) + + // ChannelTransport delivers inbound messages as soon as its read loop starts, which is before + // start() marks it operational. Creating the session first keeps the client's initialize from + // landing on a transport that would reject the session's response. + private suspend fun connect( + client: Client, + clientTransport: ChannelTransport, + server: Server, + serverTransport: ChannelTransport, + ): ServerSession { + val serverSession = server.createSession(serverTransport) + client.connect(clientTransport) + return serverSession + } + + // The motivating deadlock over real serial read loops: a server tool handler makes a + // server-to-client request (roots/list) while still handling the client's tools/call. + @Test + fun `server tool handler can call roots list mid-handling without deadlock`() = runBlocking { + val client = rootsClient() + val server = toolsServer() + val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + + val serverSession = connect(client, clientTransport, server, serverTransport) + + // Register the tools/call handler directly on the session so it can call back to the client + // mid-handling. Before concurrent dispatch this would deadlock: the server read loop is + // blocked by the tool handler and can never deliver the roots/list response. + serverSession.setRequestHandler (Method.Defined.ToolsCall) { _, _ -> + val roots = serverSession.request (ListRootsRequest()) + CallToolResult(content = listOf(TextContent(text = "roots=${roots.roots.size}"))) + } + + val result = withTimeout(10.seconds) { + client.callTool(CallToolRequest(CallToolRequestParams("needs-roots"))) + } + val text = (result as CallToolResult).content.filterIsInstance ().single().text + text shouldBe "roots=0" + + client.close() + server.close() + } + + // Client-side mirror: a client sampling handler makes a client-to-server request (tools/list) + // while still handling the server's sampling/createMessage. + @Test + fun `client sampling handler can call the server mid-handling without deadlock`() = runBlocking { + val client = Client( + clientInfo = Implementation(name = "test client", version = "1.0"), + options = ClientOptions( + capabilities = ClientCapabilities(sampling = ClientCapabilities.Sampling()), + ), + ) + val server = toolsServer() + val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + + client.setRequestHandler (Method.Defined.SamplingCreateMessage) { _, _ -> + val tools = client.request (ListToolsRequest()) + CreateMessageResult( + role = Role.Assistant, + content = TextContent(text = "tools=${tools.tools.size}"), + model = "test-model", + ) + } + + val serverSession = connect(client, clientTransport, server, serverTransport) + + val result = withTimeout(10.seconds) { + serverSession.request ( + CreateMessageRequest( + CreateMessageRequestParams( + maxTokens = 100, + messages = listOf(SamplingMessage(role = Role.User, content = TextContent(text = "hi"))), + ), + ), + ) + } + val text = result.content.filterIsInstance ().single().text + text shouldBe "tools=0" + + client.close() + server.close() + } + + // Nested-cancel cascade: cancelling the client's tools/call must reach the server tool handler + // over the wire and cascade into its nested roots/list request, cancelling the parked + // client-side roots handler. No tools/call response is ever sent. + @Test + fun `cancelling a tool call cascades into its nested roots list request`() = runBlocking { + val client = rootsClient() + val server = toolsServer() + val (clientTransport, serverTransport) = ChannelTransport.createLinkedPair() + + val capturedToolCallId = AtomicReference (null) + val toolHandlerCancelled = CompletableDeferred () + val rootsHandlerEntered = CompletableDeferred () + val rootsHandlerCancelled = CompletableDeferred () + val parkForever = CompletableDeferred () + val unexpectedToolCallResponse = CompletableDeferred () + + // Tap the server->client direction (client transport inbound) to prove no tools/call + // response is ever delivered once the call is cancelled. + clientTransport.onMessage { message -> + if (message is JSONRPCResponse && message.id == capturedToolCallId.get()) { + unexpectedToolCallResponse.complete(Unit) + } + } + + val serverSession = connect(client, clientTransport, server, serverTransport) + + // Client roots handler parks until cancelled and records the cancellation. + client.setRequestHandler (Method.Defined.RootsList) { _, _ -> + rootsHandlerEntered.complete(Unit) + try { + parkForever.await() + } catch (e: CancellationException) { + rootsHandlerCancelled.complete(e) + throw e + } + } + + // Tool handler makes a nested server->client request and records its own cancellation. + serverSession.setRequestHandler (Method.Defined.ToolsCall) { _, _ -> + val extra = currentRequestHandlerExtra()!! + capturedToolCallId.set(extra.requestId) + try { + extra.sendRequest (ListRootsRequest()) + } catch (e: CancellationException) { + toolHandlerCancelled.complete(e) + throw e + } + CallToolResult(content = listOf(TextContent(text = "unreachable"))) + } + + val call = launch { client.callTool(CallToolRequest(CallToolRequestParams("nested"))) } + + withTimeout(10.seconds) { rootsHandlerEntered.await() } + call.cancel(CancellationException("user aborted")) + + withTimeout(10.seconds) { toolHandlerCancelled.await() } + withTimeout(10.seconds) { rootsHandlerCancelled.await() } + + // The server suppresses the response for a cancelled request, so none is ever sent. + assertFalse( + unexpectedToolCallResponse.isCompleted, + "no tools/call response should be delivered after the call was cancelled", + ) + + client.close() + server.close() + } + + // Init gate plus context element in a high-level handler: initialize and tools/list are + // pipelined before notifications/initialized and answered serially in order. After the + // handshake a high-level addTool handler can observe currentRequestHandlerExtra(). + @Test + fun `pipelined initialize and tools list are processed serially then context element is available`(): Unit = + runBlocking { + val server = toolsServer() + server.addTool(name = "ctx-tool", description = "returns its request id") { + val extra = currentRequestHandlerExtra() + checkNotNull(extra) { "currentRequestHandlerExtra() must be non-null inside a tool handler" } + CallToolResult(content = listOf(TextContent(text = extra.requestId.asText()))) + } + + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val arrivalOrder = CopyOnWriteArrayList () + val pending = ConcurrentHashMap >() + clientTransport.onMessage { message -> + val id = when (message) { + is JSONRPCResponse -> message.id + is JSONRPCError -> message.id + else -> null + } + if (id != null) { + arrivalOrder.add(id) + pending[id]?.complete(message) + } + } + + // Drive the client side manually over the raw transport. + server.createSession(serverTransport) + + val initRequest = InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = "raw-client", version = "1.0"), + ), + ).toJSON() + val toolsListRequest = ListToolsRequest().toJSON() + + val initAnswered = pending.expect(initRequest.id) + val toolsListAnswered = pending.expect(toolsListRequest.id) + + // Pipelined before notifications/initialized: both handled in the serial dispatch phase. + clientTransport.send(initRequest) + clientTransport.send(toolsListRequest) + + withTimeout(10.seconds) { initAnswered.await() } + withTimeout(10.seconds) { toolsListAnswered.await() } + + // Deterministic serial ordering: initialize answered before tools/list. + assertEquals( + listOf(initRequest.id, toolsListRequest.id), + arrivalOrder.toList(), + "pipelined requests must be answered in arrival order", + ) + + // Complete the handshake, then call the high-level tool. + clientTransport.send(InitializedNotification().toJSON()) + + val toolCallRequest = CallToolRequest(CallToolRequestParams("ctx-tool")).toJSON() + val toolCallAnswered = pending.expect(toolCallRequest.id) + clientTransport.send(toolCallRequest) + + val toolCallResponse = withTimeout(10.seconds) { toolCallAnswered.await() } as JSONRPCResponse + val text = (toolCallResponse.result as CallToolResult).content.filterIsInstance ().single().text + // The handler echoed its request id, proving currentRequestHandlerExtra() was available. + text shouldBe toolCallRequest.id.asText() + } + + private fun ConcurrentHashMap >.expect( + id: RequestId, + ): CompletableDeferred = CompletableDeferred ().also { put(id, it) } + + private fun RequestId.asText(): String = when (this) { + is RequestId.StringId -> value + is RequestId.NumberId -> value.toString() + } +} diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionLoggingTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionLoggingTest.kt index 81a3b41..a1cf797 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionLoggingTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionLoggingTest.kt @@ -1,5 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.server +import io.kotest.assertions.nondeterministic.continually +import io.kotest.assertions.nondeterministic.eventually import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe @@ -17,6 +19,9 @@ import kotlinx.serialization.json.JsonPrimitive import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds class ClientConnectionLoggingTest : AbstractServerFeaturesTest() { @@ -56,7 +61,7 @@ class ClientConnectionLoggingTest : AbstractServerFeaturesTest() { @ParameterizedTest @EnumSource(LoggingLevel::class) fun `notification should filter logging messages below level`(minLevel: LoggingLevel): Unit = runBlocking { - val receivedMessages = mutableListOf () + val receivedMessages = CopyOnWriteArrayList () client.setNotificationHandler (Method.Defined.NotificationsMessage) { receivedMessages.add(it) CompletableDeferred(Unit) @@ -79,8 +84,10 @@ class ClientConnectionLoggingTest : AbstractServerFeaturesTest() { client.callTool(CallToolRequest(CallToolRequestParams("test-notification-filtered"))) + // Handlers dispatch on Dispatchers.Default after the handshake, so await the expected count + // before asserting order. val expectedLevels = LoggingLevel.entries.filter { it >= minLevel } - receivedMessages shouldHaveSize expectedLevels.size + eventually(5.seconds) { receivedMessages shouldHaveSize expectedLevels.size } receivedMessages.map { it.params.level } shouldBe expectedLevels } @@ -115,7 +122,7 @@ class ClientConnectionLoggingTest : AbstractServerFeaturesTest() { @ParameterizedTest @EnumSource(LoggingLevel::class) fun `sendLoggingMessage should filter messages below level`(minLevel: LoggingLevel): Unit = runBlocking { - val receivedMessages = mutableListOf () + val receivedMessages = CopyOnWriteArrayList () client.setNotificationHandler (Method.Defined.NotificationsMessage) { receivedMessages.add(it) CompletableDeferred(Unit) @@ -138,14 +145,16 @@ class ClientConnectionLoggingTest : AbstractServerFeaturesTest() { client.callTool(CallToolRequest(CallToolRequestParams("test-logging-level"))) + // Handlers dispatch on Dispatchers.Default after the handshake, so await the expected count + // before asserting order. val expectedLevels = LoggingLevel.entries.filter { it >= minLevel } - receivedMessages shouldHaveSize expectedLevels.size + eventually(5.seconds) { receivedMessages shouldHaveSize expectedLevels.size } receivedMessages.map { it.params.level } shouldBe expectedLevels } @Test fun `sendLoggingMessage should send no messages when level is set to highest`(): Unit = runBlocking { - val receivedMessages = mutableListOf () + val receivedMessages = CopyOnWriteArrayList () client.setNotificationHandler (Method.Defined.NotificationsMessage) { receivedMessages.add(it) CompletableDeferred(Unit) @@ -167,6 +176,7 @@ class ClientConnectionLoggingTest : AbstractServerFeaturesTest() { } client.callTool(CallToolRequest(CallToolRequestParams("test-logging-highest"))) - receivedMessages.shouldBeEmpty() + // Handlers dispatch asynchronously, so hold for a window to confirm nothing ever arrives. + continually(500.milliseconds) { receivedMessages.shouldBeEmpty() } } } diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt index 5031a9c..82cf91a 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt @@ -65,6 +65,8 @@ class ServerSessionInitializeTest { assertEquals("test-client", session.clientVersion?.name) } + // Both initialize requests arrive before notifications/initialized, in the serial dispatch + // phase, so processing and response order stay deterministic. @Test fun `should reject duplicate initialize request`() = runTest { val session = createSession() diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api index 1493a30..d856f5b 100644 --- a/kotlin-sdk-client/api/kotlin-sdk-client.api +++ b/kotlin-sdk-client/api/kotlin-sdk-client.api @@ -52,8 +52,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/ClientKt { public final class io/modelcontextprotocol/kotlin/sdk/client/ClientOptions : io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { public fun ()V - public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Z)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;ZLkotlin/coroutines/CoroutineContext;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;ZLkotlin/coroutines/CoroutineContext;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities; } diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt index 59f2b7e..3352104 100644 --- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt +++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt @@ -73,11 +73,13 @@ import kotlinx.collections.immutable.minus import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toPersistentSet import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers import kotlinx.serialization.SerializationException import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonArray +import kotlin.coroutines.CoroutineContext import kotlin.coroutines.cancellation.CancellationException private val logger = KotlinLogging.logger {} @@ -87,11 +89,16 @@ private val logger = KotlinLogging.logger {} * * @property capabilities The capabilities this client supports. * @param enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with the server. + * @param handlerCoroutineContext Coroutine context for inbound handlers. See [ProtocolOptions.handlerCoroutineContext]. */ public class ClientOptions( public val capabilities: ClientCapabilities = ClientCapabilities(), enforceStrictCapabilities: Boolean = true, -) : ProtocolOptions(enforceStrictCapabilities = enforceStrictCapabilities) + handlerCoroutineContext: CoroutineContext = Dispatchers.Default, +) : ProtocolOptions( + enforceStrictCapabilities = enforceStrictCapabilities, + handlerCoroutineContext = handlerCoroutineContext, +) /** * Initializes and connects an MCP client using the provided clientInfo [Implementation], client options, @@ -216,6 +223,7 @@ public open class Client(private val clientInfo: Implementation, options: Client serverInstructions = result.instructions notification(InitializedNotification()) + enableConcurrentDispatch() } catch (error: Throwable) { logger.error(error) { "Failed to initialize client: ${error.message}" } close() diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api index 1935e9a..28d6737 100644 --- a/kotlin-sdk-core/api/kotlin-sdk-core.api +++ b/kotlin-sdk-core/api/kotlin-sdk-core.api @@ -59,6 +59,7 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol { public abstract fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V public final fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected final fun enableConcurrentDispatch ()V public final fun getFallbackNotificationHandler ()Lkotlin/jvm/functions/Function2; public final fun getFallbackRequestHandler ()Lkotlin/jvm/functions/Function3; public final fun getNotificationHandlers ()Ljava/util/Map; @@ -71,6 +72,7 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol { public static synthetic fun notification$default (Lio/modelcontextprotocol/kotlin/sdk/shared/Protocol;Lio/modelcontextprotocol/kotlin/sdk/types/Notification;Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public fun onClose ()V public fun onError (Ljava/lang/Throwable;)V + protected fun onInitializedNotification ()V public final fun removeNotificationHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V public final fun removeRequestHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;)V public final fun request (Lio/modelcontextprotocol/kotlin/sdk/types/Request;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -84,15 +86,18 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol { public final class io/modelcontextprotocol/kotlin/sdk/shared/ProtocolKt { public static final field IMPLEMENTATION_NAME Ljava/lang/String; + public static final fun currentRequestHandlerExtra (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun getDEFAULT_REQUEST_TIMEOUT ()J } public class io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptions { - public synthetic fun (ZJILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (ZJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZJLkotlin/coroutines/CoroutineContext;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZJLkotlin/coroutines/CoroutineContext;IILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getEnforceStrictCapabilities ()Z + public final fun getHandlerCoroutineContext ()Lkotlin/coroutines/CoroutineContext; public final fun getTimeout-UwyO8pc ()J public final fun setEnforceStrictCapabilities (Z)V + public final fun setHandlerCoroutineContext (Lkotlin/coroutines/CoroutineContext;)V public final fun setTimeout-LRDsOJo (J)V } @@ -107,8 +112,16 @@ public final class io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferKt { public static final fun serializeMessage (Lio/modelcontextprotocol/kotlin/sdk/types/JSONRPCMessage;)Ljava/lang/String; } -public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra { - public fun ()V +public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra : kotlin/coroutines/AbstractCoroutineContextElement { + public static final field Key Lio/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra$Key; + public final fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method; + public final fun getRequestId ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestId; + public final fun sendNotification (Lio/modelcontextprotocol/kotlin/sdk/types/Notification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun sendRequest (Lio/modelcontextprotocol/kotlin/sdk/types/Request;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun sendRequest$default (Lio/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra;Lio/modelcontextprotocol/kotlin/sdk/types/Request;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestHandlerExtra$Key : kotlin/coroutines/CoroutineContext$Key { } public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestOptions : io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions { diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 50dc356..3515b4a 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -27,15 +27,33 @@ import kotlinx.atomicfu.AtomicRef import kotlinx.atomicfu.atomic import kotlinx.atomicfu.getAndUpdate import kotlinx.atomicfu.update +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.encodeToJsonElement +import kotlin.coroutines.AbstractCoroutineContextElement +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -53,17 +71,26 @@ public typealias ProgressCallback = (Progress) -> Unit /** * Additional initialization options. * - * @property enforceStrictCapabilities whether to restrict emitted requests to only those that the - * remote side has indicated it can handle through its advertised capabilities. - * This does NOT affect checking of _local_ side capabilities, as it is considered a logic error - * to mis-specify those. - * Currently defaults to `false` for backwards compatibility with SDK versions that did not advertise - * capabilities correctly; in the future, this will default to `true`. + * [handlerCoroutineContext] is read once when [Protocol.connect] attaches a transport and stays + * fixed until the next [Protocol.connect]. + * + * @property enforceStrictCapabilities whether to restrict emitted requests to only those the remote + * side advertised support for. Local-side capability checks are unaffected. Defaults to `false` for + * backward compatibility with SDK versions that did not advertise capabilities correctly. * @property timeout default timeout for outgoing requests + * @property handlerCoroutineContext coroutine context used to run inbound request and notification + * handlers once initialization has completed. Must contain a real dispatching + * [kotlin.coroutines.ContinuationInterceptor]. `Dispatchers.Unconfined` and unconfined test + * dispatchers are unsupported, since handler resumptions would then run on the transport read loop + * and reintroduce head-of-line blocking. Any [kotlinx.coroutines.Job] in this context is ignored. */ public open class ProtocolOptions( public var enforceStrictCapabilities: Boolean = false, public var timeout: Duration = DEFAULT_REQUEST_TIMEOUT, + public var handlerCoroutineContext: CoroutineContext = Dispatchers.Default, + // Internal handler-concurrency bounds, read once at connect and not part of the public surface. + internal var maxConcurrentHandlers: Int = DEFAULT_MAX_CONCURRENT_HANDLERS, + internal var maxInFlightHandlers: Int = DEFAULT_MAX_IN_FLIGHT_HANDLERS, ) /** @@ -71,6 +98,12 @@ public open class ProtocolOptions( */ public val DEFAULT_REQUEST_TIMEOUT: Duration = 60.seconds +/** Default cap on concurrently executing inbound handlers per connection. */ +internal const val DEFAULT_MAX_CONCURRENT_HANDLERS: Int = 64 + +/** Default cap on launched-but-not-completed inbound handlers per connection. */ +internal const val DEFAULT_MAX_IN_FLIGHT_HANDLERS: Int = 256 + /** * Options that can be given per request. * @@ -121,7 +154,7 @@ public class RequestOptions( override fun hashCode(): Int { var result = super.hashCode() - result = 31 * result + (onProgress?.hashCode() ?: 0) + result = 31 * result + onProgress.hashCode() result = 31 * result + timeout.hashCode() return result } @@ -132,11 +165,70 @@ public class RequestOptions( /** * Extra data given to request handlers. + * + * One instance exists per in-flight inbound request. It is passed as the second parameter to + * low-level handlers registered via [Protocol.setRequestHandler] **and** installed into the + * handler coroutine's [CoroutineContext], so code nested anywhere under the handler can reach it + * via [currentRequestHandlerExtra] without parameter plumbing. + * + * @property requestId the JSON-RPC id of the request being handled + * @property method the request's method */ -public class RequestHandlerExtra +public class RequestHandlerExtra internal constructor( + public val requestId: RequestId, + public val method: Method, + private val protocol: Protocol, + internal val capturedTransport: Transport, +) : AbstractCoroutineContextElement(Key) { + + /** Context key for retrieving the extra from a handler coroutine's context. */ + public companion object Key : CoroutineContext.Key + + /** + * Sends a notification tagged with `relatedRequestId = `[requestId], letting transports + * associate it with the request being handled (e.g. route it onto the request's SSE stream). + */ + public suspend fun sendNotification(notification: Notification) { + protocol.notification(notification, relatedRequestId = requestId) + } + + /** + * Sends a request to the peer tagged with `relatedRequestId = `[requestId]. + * + * Mirrors [Protocol.request]; cancellation of the handler coroutine propagates into this call. + */ + public suspend fun sendRequest(request: Request, options: RequestOptions? = null): T { + val base = options ?: RequestOptions() + return protocol.request( + request, + RequestOptions( + relatedRequestId = requestId, + resumptionToken = base.resumptionToken, + onResumptionToken = base.onResumptionToken, + onProgress = base.onProgress, + timeout = base.timeout, + ), + ) + } +} + +/** The [RequestHandlerExtra] of the MCP request being handled by the current coroutine, or `null` outside a handler. */ +public suspend fun currentRequestHandlerExtra(): RequestHandlerExtra? = currentCoroutineContext()[RequestHandlerExtra] internal val COMPLETED = CompletableDeferred(Unit).also { it.complete(Unit) } +/** Cap on how many recently-cancelled outbound request ids are remembered to quiet late responses/progress. */ +private const val CANCELLED_REQUEST_IDS_REMEMBERED = 256 + +// Bypass set: exempt from both bound tiers so the connection cannot self-block. +// Responses are not listed — they are handled inline before dispatch. +private val CONTROL_METHODS: Set = setOf( + Method.Defined.Ping.value, + Method.Defined.NotificationsCancelled.value, + Method.Defined.NotificationsProgress.value, + Method.Defined.NotificationsInitialized.value, +) + /** * Implements MCP protocol framing on top of a pluggable transport, including * features like request/response linking, notifications, and progress. @@ -144,9 +236,11 @@ internal val COMPLETED = CompletableDeferred(Unit).also { it.complete(Unit) } * @property options protocol-level configuration; `null` falls back to defaults */ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptions?) { + private val connectionRef: AtomicRef = atomic(null) + /** The active transport, or `null` if not connected. */ - public var transport: Transport? = null - private set + public val transport: Transport? + get() = connectionRef.value?.transport private val _requestHandlers: AtomicRef RequestResult?>> = @@ -184,6 +278,21 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio public val progressHandlers: Map get() = _progressHandlers.value + private val recentlyCancelledRequestIds: AtomicRef > = + atomic(persistentListOf()) + + /** Remembers a locally-cancelled outbound request id so its late response/progress is quieted. */ + private fun rememberCancelledRequestId(id: RequestId) { + recentlyCancelledRequestIds.update { current -> + val appended = current.adding(id) + if (appended.size > CANCELLED_REQUEST_IDS_REMEMBERED) appended.removingAt(0) else appended + } + } + + /** Whether [id] was recently cancelled locally (see [rememberCancelledRequestId]). */ + private fun isRecentlyCancelled(id: RequestId?): Boolean = + id != null && recentlyCancelledRequestIds.value.contains(id) + /** * Callback for when the connection is closed for any reason. * @@ -213,6 +322,11 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio public var fallbackNotificationHandler: (suspend (notification: JSONRPCNotification) -> Unit)? = null init { + setNotificationHandler (Method.Defined.NotificationsCancelled) { notification -> + handleCancelledNotification(notification) + COMPLETED + } + setNotificationHandler (Method.Defined.NotificationsProgress) { notification -> onProgress(notification) COMPLETED @@ -223,15 +337,64 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } } + /** + * Enables concurrent dispatch of inbound messages on the current connection. + * + * Called by subclasses when the MCP initialization phase completes (the client after the + * initialize exchange; the server session on receiving `notifications/initialized`). Until + * then, inbound messages are handled inline in arrival order. Reset by [connect]. + */ + protected fun enableConcurrentDispatch() { + val connection = connectionRef.value ?: return + connection.concurrentDispatchEnabled.value = true + } + + /** + * Router hook invoked whenever `notifications/initialized` is received, before handler lookup. + * + * Runs regardless of which notification handler is registered for the method, so replacing the + * handler via [setNotificationHandler] cannot disable subclass initialization logic. + */ + protected open fun onInitializedNotification() {} + /** * Attaches to the given transport, starts it, and starts listening for messages. * - * The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward. + * The Protocol object assumes ownership of the Transport, replacing any callbacks that have + * already been set, and expects that it is the only user of the Transport instance going forward. + * + * @throws IllegalStateException if this Protocol is already connected; call [close] first. */ public open suspend fun connect(transport: Transport) { - this.transport = transport + val maxConcurrent = options?.maxConcurrentHandlers ?: DEFAULT_MAX_CONCURRENT_HANDLERS + val maxInFlight = options?.maxInFlightHandlers ?: DEFAULT_MAX_IN_FLIGHT_HANDLERS + require(maxConcurrent > 0) { "maxConcurrentHandlers must be positive, but was $maxConcurrent" } + require(maxInFlight >= maxConcurrent) { + "maxInFlightHandlers ($maxInFlight) must be >= maxConcurrentHandlers ($maxConcurrent)" + } + val handlerContext = (options?.handlerCoroutineContext ?: Dispatchers.Default).minusKey(Job) + val interceptor = handlerContext[ContinuationInterceptor] + if (interceptor === Dispatchers.Unconfined || + interceptor?.let { it::class.simpleName?.contains("Unconfined") } == true + ) { + logger.warn { + "handlerCoroutineContext uses an unconfined dispatcher ($interceptor); " + + "handler resumptions will run on the transport read loop and may block unrelated messages" + } + } + val connection = Connection( + transport = transport, + handlerScope = CoroutineScope(SupervisorJob() + handlerContext + CoroutineName("McpProtocol")), + executionSemaphore = Semaphore(maxConcurrent), + maxInFlightHandlers = maxInFlight, + ) + if (!connectionRef.compareAndSet(null, connection)) { + connection.handlerScope.cancel() + error("Protocol is already connected; close() the current connection before connecting a new transport") + } + transport.onClose { - doClose() + doClose(connection) } transport.onError { @@ -239,24 +402,42 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } transport.onMessage { message -> + if (connectionRef.value !== connection) { + logger.trace { "Dropping message received after close: ${message::class.simpleName}" } + return@onMessage + } when (message) { is JSONRPCResponse -> onResponse(message, null) - is JSONRPCRequest -> onRequest(message) - is JSONRPCNotification -> onNotification(message) + is JSONRPCRequest -> dispatchRequest(connection, message) + is JSONRPCNotification -> dispatchNotification(connection, message) is JSONRPCError -> onResponse(null, message) is JSONRPCEmptyMessage -> Unit } } logger.info { "Starting transport" } - transport.start() + try { + transport.start() + } catch (cause: Throwable) { + // Roll back so the Protocol can be reconnected after a failed start; idempotent with + // a transport-initiated doClose (both CAS on the same connection). + if (connectionRef.compareAndSet(connection, null)) { + connection.handlerScope.cancel() + } + throw cause + } } - private fun doClose() { - val handlersToNotify = _responseHandlers.value.values.toList() - _responseHandlers.getAndSet(persistentMapOf()) + private fun doClose(connection: Connection) { + // A stale onClose from a previous transport must not tear down the successor connection. + if (!connectionRef.compareAndSet(connection, null)) { + logger.trace { "Ignoring close signal from a stale transport" } + return + } + val handlersToNotify = _responseHandlers.getAndSet(persistentMapOf()).values.toList() _progressHandlers.getAndSet(persistentMapOf()) - transport = null + connection.inFlightRequestJobs.getAndSet(persistentMapOf()) + connection.handlerScope.cancel() onClose() val error = McpException(RPCError.ErrorCode.CONNECTION_CLOSED, "Connection closed") @@ -265,6 +446,110 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } } + private suspend fun dispatchRequest(connection: Connection, request: JSONRPCRequest) { + if (!connection.concurrentDispatchEnabled.value) { + // Serial phase (before the peer completes MCP initialization): inline, in arrival + // order — the delivering coroutine awaits the handler. + onRequest(request, connection, trackCancellation = false) + return + } + val bypass = request.method in CONTROL_METHODS + if (!bypass && !tryAdmit(connection)) { + rejectOverloadedRequest(connection, request) + return + } + // UNDISPATCHED starts the handler synchronously in the delivering coroutine, preserving + // arrival order, then moves to handlerCoroutineContext at its first suspension. + connection.handlerScope.launch(start = CoroutineStart.UNDISPATCHED) { + // Register before any suspension point so a following notifications/cancelled can find + // and cancel even a handler still parked on admission. + registerInFlight(connection, request.id, coroutineContext.job) + try { + if (bypass) { + onRequest(request, connection, trackCancellation = true) + } else { + connection.executionSemaphore.withPermit { + onRequest(request, connection, trackCancellation = true) + } + } + } finally { + if (!bypass) connection.inFlightCount.decrementAndGet() + } + } + } + + private suspend fun dispatchNotification(connection: Connection, notification: JSONRPCNotification) { + if (notification.method == Method.Defined.NotificationsInitialized.value) { + onInitializedNotification() + } + if (!connection.concurrentDispatchEnabled.value) { + onNotification(notification) + return + } + val bypass = notification.method in CONTROL_METHODS + if (!bypass && !tryAdmit(connection)) { + val dropped = IllegalStateException( + "Dropped notification ${notification.method}: too many in-flight messages", + ) + logger.warn { dropped.message } + onError(dropped) + return + } + connection.handlerScope.launch(start = CoroutineStart.UNDISPATCHED) { + try { + if (bypass) { + onNotification(notification) + } else { + connection.executionSemaphore.withPermit { onNotification(notification) } + } + } finally { + if (!bypass) connection.inFlightCount.decrementAndGet() + } + } + } + + private fun tryAdmit(connection: Connection): Boolean { + while (true) { + val current = connection.inFlightCount.value + if (current >= connection.maxInFlightHandlers) return false + if (connection.inFlightCount.compareAndSet(current, current + 1)) return true + } + } + + private suspend fun rejectOverloadedRequest(connection: Connection, request: JSONRPCRequest) { + logger.warn { + "Rejecting request ${request.method} (id: ${request.id}): " + + "in-flight handler limit (${connection.maxInFlightHandlers}) reached" + } + try { + connection.transport.send( + JSONRPCError( + id = request.id, + error = RPCError( + code = RPCError.ErrorCode.INTERNAL_ERROR, + message = "Server is busy: too many in-flight messages", + ), + ), + ) + } catch (e: CancellationException) { + throw e + } catch (cause: Throwable) { + logger.error(cause) { "Failed to send busy rejection for request id: ${request.id}" } + onError(cause) + } + } + + private fun handleCancelledNotification(notification: CancelledNotification) { + val connection = connectionRef.value ?: return + val requestId = notification.params.requestId + val job = connection.inFlightRequestJobs.value[requestId] + if (job == null) { + logger.trace { "Ignoring cancellation for unknown or already-completed request: $requestId" } + return + } + job.cancel("Cancelled by peer: ${notification.params.reason ?: "unknown"}") + } + private suspend fun onNotification(notification: JSONRPCNotification) { logger.trace { "Received notification: ${notification.method}" } @@ -284,15 +569,16 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } } - private suspend fun onRequest(request: JSONRPCRequest) { + private suspend fun onRequest(request: JSONRPCRequest, connection: Connection, trackCancellation: Boolean) { logger.trace { "Received request: ${request.method} (id: ${request.id})" } val handler = requestHandlers[request.method] ?: fallbackRequestHandler + val capturedTransport = connection.transport if (handler === null) { logger.trace { "No handler found for request: ${request.method}" } try { - transport?.send( + capturedTransport.send( JSONRPCError( id = request.id, error = RPCError( @@ -310,36 +596,77 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio return } - try { - val result = handler(request, RequestHandlerExtra()) - logger.trace { "Request handled successfully: ${request.method} (id: ${request.id})" } + val extra = RequestHandlerExtra( + requestId = request.id, + method = Method.from(request.method), + protocol = this, + capturedTransport = capturedTransport, + ) - transport?.send( - JSONRPCResponse( - id = request.id, - result = result ?: EmptyResult(), - ), - ) + // Registration in the in-flight registry already happened in dispatchRequest's launch + // block (synchronously, pre-suspension). Here we only need the job for suppression checks. + val handlerJob = if (trackCancellation) currentCoroutineContext().job else null + + withContext(extra) { + try { + val result = handler(request, extra) + logger.trace { "Request handled successfully: ${request.method} (id: ${request.id})" } + if (handlerJob?.isCancelled == true) { + logger.trace { "Suppressing response for request cancelled by peer (id: ${request.id})" } + return@withContext + } + capturedTransport.send( + JSONRPCResponse( + id = request.id, + result = result ?: EmptyResult(), + ), + ) + } catch (e: CancellationException) { + // Intentional exception to the always-rethrow-CE convention, only at this boundary. + if (handlerJob == null) throw e // serial phase: inline dispatch is not response-suppressed + if (handlerJob.isCancelled) throw e // genuine peer or close cancel: suppress the response + // CE escaped a live handler job (e.g. a leaked inner withTimeout). Answer + // INTERNAL_ERROR so the peer does not hang until its own timeout. + respondWithError(capturedTransport, request, e) + } catch (cause: Throwable) { + if (handlerJob?.isCancelled == true) { + logger.trace { "Suppressing error response for request cancelled by peer (id: ${request.id})" } + return@withContext + } + respondWithError(capturedTransport, request, cause) + } + } + } + + private fun registerInFlight(connection: Connection, id: RequestId, job: Job) { + val previous = connection.inFlightRequestJobs.getAndUpdate { current -> current.putting(id, job) }[id] + if (previous != null) { + logger.warn { "Duplicate in-flight request id $id; replacing the previous handler job" } + } + job.invokeOnCompletion { + // Identity guard: never evict a successor job registered under a reused id. + connection.inFlightRequestJobs.update { current -> + if (current[id] === job) current.removing(id) else current + } + } + } + + private suspend fun respondWithError(transport: Transport, request: JSONRPCRequest, cause: Throwable) { + logger.error(cause) { "Error handling request: ${request.method} (id: ${request.id})" } + try { + val rpcError = if (cause is McpException) { + RPCError(code = cause.code, message = cause.message.orEmpty(), data = cause.data) + } else { + RPCError(code = RPCError.ErrorCode.INTERNAL_ERROR, message = cause.message ?: "Internal error") + } + transport.send(JSONRPCError(id = request.id, error = rpcError)) } catch (e: CancellationException) { throw e - } catch (cause: Throwable) { - logger.error(cause) { "Error handling request: ${request.method} (id: ${request.id})" } - - try { - val rpcError = if (cause is McpException) { - RPCError(code = cause.code, message = cause.message.orEmpty(), data = cause.data) - } else { - RPCError(code = RPCError.ErrorCode.INTERNAL_ERROR, message = cause.message ?: "Internal error") - } - transport?.send(JSONRPCError(id = request.id, error = rpcError)) - } catch (e: CancellationException) { - throw e - } catch (sendError: Throwable) { - logger.error(sendError) { - "Failed to send error response for request: ${request.method} (id: ${request.id})" - } - // Optionally implement fallback behavior here + } catch (sendError: Throwable) { + logger.error(sendError) { + "Failed to send error response for request: ${request.method} (id: ${request.id})" } + onError(sendError) } } @@ -354,6 +681,10 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio val handler = _progressHandlers.value[progressToken] if (handler == null) { + if (isRecentlyCancelled(progressToken)) { + logger.trace { "Ignoring progress for a locally cancelled request: $progressToken" } + return + } val error = Error( "Received a progress notification for an unknown token: ${McpJson.encodeToString(notification)}", ) @@ -370,7 +701,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio val oldResponseHandlers = _responseHandlers.getAndUpdate { current -> if (messageId != null && messageId in current) { - current.remove(messageId) + current.removing(messageId) } else { current } @@ -379,8 +710,12 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio val handler = oldResponseHandlers[messageId] if (handler != null) { - messageId?.let { msg -> _progressHandlers.update { it.remove(msg) } } + messageId?.let { msg -> _progressHandlers.update { it.removing(msg) } } } else { + if (isRecentlyCancelled(messageId)) { + logger.trace { "Ignoring response for a locally cancelled request: $messageId" } + return + } onError( IllegalStateException( "Received a response for an unknown message ID: ${McpJson.encodeToString(error ?: response)}", @@ -438,7 +773,8 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio public suspend fun request(request: Request, options: RequestOptions? = null): T { logger.trace { "Sending request: ${request.method}" } val result = CompletableDeferred () - val transport = transport ?: error("Not connected") + val connection = connectionRef.value ?: error("Not connected") + val transport = connection.transport if (this@Protocol.options?.enforceStrictCapabilities == true) { assertCapabilityForMethod(request.method) @@ -448,7 +784,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio options?.onProgress?.let { progressHandler -> logger.trace { "Registering progress handler for request id: $id" } _progressHandlers.update { current -> - current.put(id, progressHandler) + current.putting(id, progressHandler) } val paramsObject = (this.params as? JsonObject) ?: JsonObject(emptyMap()) @@ -467,10 +803,10 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio val jsonRpcRequestId = jsonRpcRequest.id _responseHandlers.update { current -> - current.put(jsonRpcRequestId) { response, error -> + current.putting(jsonRpcRequestId) { response, error -> if (error != null) { result.completeExceptionally(error) - return@put + return@putting } try { @@ -482,42 +818,84 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } } - val cancel: suspend (Throwable) -> Unit = { reason: Throwable -> - _responseHandlers.update { current -> current.remove(jsonRpcRequestId) } - _progressHandlers.update { current -> current.remove(jsonRpcRequestId) } + if (connectionRef.value !== connection) { + // Connection closed or replaced during registration; nothing would complete this handler. + _responseHandlers.update { it.removing(jsonRpcRequestId) } + _progressHandlers.update { it.removing(jsonRpcRequestId) } + throw McpException(RPCError.ErrorCode.CONNECTION_CLOSED, "Connection closed") + } - val notification = CancelledNotification( - params = CancelledNotificationParams( - requestId = jsonRpcRequestId, - reason = reason.message ?: "Unknown", - ), - ) + val cancelPending: suspend (reason: Throwable, notifyPeer: Boolean) -> Unit = { reason, notifyPeer -> + // Remember before removing: a racing onResponse/onProgress that sees the handler gone + // must also see the id, or it reports a late message as unknown. Order is load-bearing. + rememberCancelledRequestId(jsonRpcRequestId) + _responseHandlers.update { current -> current.removing(jsonRpcRequestId) } + _progressHandlers.update { current -> current.removing(jsonRpcRequestId) } - val jsonRpcNotification = notification.toJSON() - - transport.send(jsonRpcNotification, options) + if (notifyPeer) { + val notification = CancelledNotification( + params = CancelledNotificationParams( + requestId = jsonRpcRequestId, + reason = reason.message ?: "Unknown", + ), + ) + transport.send(notification.toJSON(), options) + } result.completeExceptionally(reason) } + // The MCP spec forbids cancelling `initialize`; local cleanup still runs, + // but no notifications/cancelled goes on the wire (timeout and cancel paths alike). + val notifyPeerOnCancel = request.method != Method.Defined.Initialize + val timeout = options?.timeout ?: DEFAULT_REQUEST_TIMEOUT try { - withTimeout(timeout) { + val response = withTimeoutOrNull(timeout) { logger.trace { "Sending request message with id: $jsonRpcRequestId" } - this@Protocol.transport?.send(jsonRpcRequest, options) + transport.send(jsonRpcRequest, options) + result.await() } - return result.await() - } catch (cause: TimeoutCancellationException) { - logger.error { "Request timed out after ${timeout.inWholeMilliseconds}ms: ${request.method}" } - cancel( - McpException( + if (response == null) { + // Our own request timeout expired. An outer withTimeout's cancellation propagates + // through withTimeoutOrNull instead of returning null and is handled by the catch below. + logger.error { "Request timed out after ${timeout.inWholeMilliseconds}ms: ${request.method}" } + val timeoutError = McpException( code = RPCError.ErrorCode.REQUEST_TIMEOUT, message = "Request timed out", data = JsonObject(mutableMapOf("timeout" to JsonPrimitive(timeout.inWholeMilliseconds))), - ), - ) - result.cancel(cause) + ) + withContext(NonCancellable) { + try { + cancelPending(timeoutError, notifyPeerOnCancel) + } catch (e: Throwable) { + logger.warn(e) { "Failed to notify peer about timed-out request" } + onError(e) + } + } + result.cancel() + throw timeoutError + } + return response + } catch (cause: CancellationException) { + // Plain caller cancellation, or an outer withTimeout that propagated through + // withTimeoutOrNull (only our own deadline returns null there). NonCancellable so the + // cancellation notification still goes out even though the caller's job is cancelled. + // A secondary send failure must never replace the original CancellationException. + withContext(NonCancellable) { + try { + cancelPending(cause, notifyPeerOnCancel) + } catch (e: Throwable) { + logger.warn(e) { "Failed to notify peer about cancelled request" } + onError(e) + } + } throw cause + } finally { + // Backstop for the failed-send path. The success, timeout and cancellation paths already + // removed these handlers, so this only bites when the outbound send failed. + _responseHandlers.update { it.removing(jsonRpcRequestId) } + _progressHandlers.update { it.removing(jsonRpcRequestId) } } } @@ -538,6 +916,11 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio * Registers a handler to invoke when this protocol object receives a request with the given method. * * Note that this will replace any previous request handler for the same method. + * + * Handlers may run concurrently after initialization and are cancelled cooperatively, so always + * re-throw [CancellationException]. Replacing a built-in control-method handler (`ping`, + * `notifications/cancelled`, `notifications/progress`, `notifications/initialized`) with slow + * code degrades connection liveness. */ public inline fun setRequestHandler( method: Method, @@ -556,7 +939,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio val wrapped = wrapRequestHandler(method, block) _requestHandlers.update { current -> - current.put(method.value) { jSONRPCRequest, extraHandler -> + current.putting(method.value) { jSONRPCRequest, extraHandler -> val request = jSONRPCRequest.fromJSON() val response = wrapped(request as T, extraHandler) response @@ -582,17 +965,23 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio * Removes the request handler for the given method. */ public fun removeRequestHandler(method: Method) { - _requestHandlers.update { current -> current.remove(method.value) } + _requestHandlers.update { current -> current.removing(method.value) } } /** * Registers a handler to invoke when this protocol object receives a notification with the given method. * * Note that this will replace any previous notification handler for the same method. + * + * Handlers may run concurrently after initialization and are cancelled cooperatively, so always + * re-throw [CancellationException]. Replacing a built-in control-method handler (`ping`, + * `notifications/cancelled`, `notifications/progress`, `notifications/initialized`) with slow + * code degrades connection liveness. Replacing `notifications/cancelled` in particular disables + * inbound request cancellation entirely, since that handler is what cancels the running job. */ public fun setNotificationHandler(method: Method, handler: (notification: T) -> Deferred ) { _notificationHandlers.update { current -> - current.put(method.value) { + current.putting(method.value) { @Suppress("UNCHECKED_CAST") handler(it.fromJSON() as T) } @@ -603,6 +992,22 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio * Removes the notification handler for the given method. */ public fun removeNotificationHandler(method: Method) { - _notificationHandlers.update { current -> current.remove(method.value) } + _notificationHandlers.update { current -> current.removing(method.value) } + } + + private class Connection( + val transport: Transport, + val handlerScope: CoroutineScope, + val executionSemaphore: Semaphore, + val maxInFlightHandlers: Int, + ) { + /** Init gate: inbound dispatch is inline/serial until this flips. */ + val concurrentDispatchEnabled = atomic(false) + + /** Admission tier: launched-but-not-completed handler jobs (running + parked). */ + val inFlightCount = atomic(0) + + /** In-flight request handler jobs, for `notifications/cancelled`. */ + val inFlightRequestJobs: AtomicRef > = atomic(persistentMapOf()) } } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt index 19f4da5..90403e8 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt @@ -53,4 +53,13 @@ public sealed interface Method { */ @Serializable public data class Custom(override val value: String) : Method + + public companion object { + private val definedByValue: Map by lazy { + Defined.entries.associateBy { it.value } + } + + /** Resolves a wire method string to a [Defined] entry when known, [Custom] otherwise. */ + internal fun from(value: String): Method = definedByValue[value] ?: Custom(value) + } } diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt index ab85e6d..6eab14a 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt @@ -59,18 +59,11 @@ internal object MethodSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("io.modelcontextprotocol.kotlin.sdk.types.Method", PrimitiveKind.STRING) - private val definedMethods: Map by lazy { - Method.Defined.entries.associateBy { it.value } - } - override fun serialize(encoder: Encoder, value: Method) { encoder.encodeString(value.value) } - override fun deserialize(decoder: Decoder): Method { - val decodedString = decoder.decodeString() - return definedMethods[decodedString] ?: Method.Custom(decodedString) - } + override fun deserialize(decoder: Decoder): Method = Method.from(decoder.decodeString()) } // ============================================================================ diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyTest.kt new file mode 100644 index 0000000..b3c95ac --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyTest.kt @@ -0,0 +1,524 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +class ProtocolConcurrencyTest { + + private fun TestScope.newProtocol(): TestProtocol = + TestProtocol(ProtocolOptions(handlerCoroutineContext = StandardTestDispatcher(testScheduler))) + + @Test + fun `a suspended slow handler does not block a later fast request`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/slow") delay(10.seconds) + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/slow")) + transport.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/fast")) + runCurrent() + + responsesOn(transport).map { it.id } shouldBe listOf(RequestId(2L)) + + advanceTimeBy(11.seconds) + runCurrent() + responsesOn(transport).map { it.id } shouldBe listOf(RequestId(2L), RequestId(1L)) + } + + @Test + fun `dispatch is serial until concurrent dispatch is enabled`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + // gate NOT flipped + + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/slow") delay(10.seconds) + EmptyResult() + } + + val delivery = launch { + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/slow")) + transport.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/fast")) + } + runCurrent() + // inline handling: the delivering coroutine is parked inside the slow handler + delivery.isCompleted shouldBe false + responsesOn(transport) shouldBe emptyList() + + advanceTimeBy(11.seconds) + runCurrent() + delivery.isCompleted shouldBe true + responsesOn(transport).map { it.id } shouldBe listOf(RequestId(1L), RequestId(2L)) + } + + // the hook fires before handler lookup and cannot be disabled by a user handler + @Test + fun `initialized notification invokes the router hook even when a user handler is registered`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + var userHandlerCalled = false + protocol.setNotificationHandler (Method.Defined.NotificationsInitialized) { + userHandlerCalled = true + COMPLETED + } + transport.deliver(JSONRPCNotification(method = Method.Defined.NotificationsInitialized.value)) + runCurrent() + + protocol.initializedNotificationCount shouldBe 1 + userHandlerCalled shouldBe true + } + + @Test + fun `cancelled notification cancels the in-flight handler and suppresses the response`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val entered = CompletableDeferred () + val sawCancellation = CompletableDeferred () + protocol.fallbackRequestHandler = { _, _ -> + entered.complete(Unit) + try { + delay(10.seconds) + } catch (e: CancellationException) { + sawCancellation.complete(Unit) + throw e + } + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(5L), method = "test/slow")) + runCurrent() + entered.await() + + transport.deliver(cancelledNotification(requestId = RequestId(5L), reason = "user gave up")) + runCurrent() + sawCancellation.await() + advanceUntilIdle() + + responsesOn(transport) shouldBe emptyList() + errorsOn(transport) shouldBe emptyList() // JSONRPCError messages on the wire + protocol.errors shouldBe emptyList() + } + + @Test + fun `cancelled notification for an unknown id is a silent no-op`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + transport.deliver(cancelledNotification(requestId = RequestId(404L), reason = "nope")) + advanceUntilIdle() + + protocol.errors shouldBe emptyList() + } + + @Test + fun `handler leaking CancellationException without being cancelled produces INTERNAL_ERROR`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + protocol.fallbackRequestHandler = { _, _ -> throw CancellationException("leaked") } + + transport.deliver(JSONRPCRequest(id = RequestId(6L), method = "test/leak")) + advanceUntilIdle() + + val error = errorsOn(transport).single() + error.id shouldBe RequestId(6L) + error.error.code shouldBe RPCError.ErrorCode.INTERNAL_ERROR + } + + // suppression race variants + @Test + fun `handler that swallows cancellation and returns normally still gets no response`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val entered = CompletableDeferred () + protocol.fallbackRequestHandler = { _, _ -> + entered.complete(Unit) + try { + delay(10.seconds) + } catch (_: CancellationException) { + // swallow (anti-pattern) and return normally + } + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(7L), method = "test/swallow")) + runCurrent() + entered.await() + transport.deliver(cancelledNotification(requestId = RequestId(7L), reason = "stop")) + advanceUntilIdle() + + responsesOn(transport) shouldBe emptyList() + } + + @Test + fun `handler throwing non-CE while cancelled gets no error response`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val entered = CompletableDeferred () + protocol.fallbackRequestHandler = { _, _ -> + entered.complete(Unit) + try { + delay(10.seconds) + } catch (_: CancellationException) { + throw IllegalStateException("boom") + } + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(8L), method = "test/throw")) + runCurrent() + entered.await() + transport.deliver(cancelledNotification(requestId = RequestId(8L), reason = "stop")) + advanceUntilIdle() + + errorsOn(transport) shouldBe emptyList() + protocol.errors shouldBe emptyList() + } + + @Test + fun `suspension-free notification handlers observe strict arrival order`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val seen = mutableListOf () + protocol.fallbackNotificationHandler = { notification -> + seen.add((notification.params as JsonObject).getValue("i").jsonPrimitive.int) + } + + repeat(20) { i -> + transport.deliver( + JSONRPCNotification(method = "test/event", params = buildJsonObject { put("i", i) }), + ) + } + advanceUntilIdle() + + seen shouldBe (0 until 20).toList() + } + + @Test + fun `reconnect resets the dispatch gate to serial`() = runTest { + val protocol = newProtocol() + val transport1 = RecordingTransport() + protocol.connect(transport1) + protocol.enableConcurrency() + protocol.close() + + val transport2 = RecordingTransport() + protocol.connect(transport2) + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/slow") delay(10.seconds) + EmptyResult() + } + + val delivery = launch { + transport2.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/slow")) + transport2.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/fast")) + } + runCurrent() + delivery.isCompleted shouldBe false // serial again: gate reset + advanceTimeBy(11.seconds) + runCurrent() + responsesOn(transport2).map { it.id } shouldBe listOf(RequestId(1L), RequestId(2L)) + } + + @Test + fun `close cancels in-flight handlers suppresses responses and fails pending outbound requests`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val entered = CompletableDeferred () + val sawCancellation = CompletableDeferred () + protocol.fallbackRequestHandler = { _, _ -> + entered.complete(Unit) + try { + delay(10.seconds) + } catch (e: CancellationException) { + sawCancellation.complete(Unit) + throw e + } + EmptyResult() + } + transport.deliver(JSONRPCRequest(id = RequestId(9L), method = "test/slow")) + runCurrent() + entered.await() + + // shouldThrow inside the async so its failure does not cancel the (non-supervisor) test scope. + val pending = async { shouldThrow { protocol.request (PingRequest()) } } + transport.awaitRequest() // outbound ping on the wire + + protocol.close() + runCurrent() + + sawCancellation.await() + responsesOn(transport) shouldBe emptyList() + val thrown = pending.await() + thrown.code shouldBe RPCError.ErrorCode.CONNECTION_CLOSED + } + + @Test + fun `message delivered after close is dropped without crash or response`() = runTest { + val protocol = newProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + protocol.close() + + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/anything")) + advanceUntilIdle() + + responsesOn(transport) shouldBe emptyList() + errorsOn(transport) shouldBe emptyList() + } + + @Test + fun `at most maxConcurrentHandlers handlers run concurrently while control messages bypass`() = runTest { + val protocol = TestProtocol( + ProtocolOptions( + handlerCoroutineContext = StandardTestDispatcher(testScheduler), + maxConcurrentHandlers = 2, + ), + ) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + var entered = 0 + val release = CompletableDeferred () + protocol.fallbackRequestHandler = { _, _ -> + entered += 1 + release.await() + EmptyResult() + } + + repeat(4) { i -> transport.deliver(JSONRPCRequest(id = RequestId(i.toLong()), method = "test/slow")) } + runCurrent() + entered shouldBe 2 // exactly two running, two parked on the semaphore + + // ping (bypass) is answered while saturated + transport.deliver(JSONRPCRequest(id = RequestId(100L), method = Method.Defined.Ping.value)) + runCurrent() + responsesOn(transport).map { it.id } shouldBe listOf(RequestId(100L)) + + // responses (bypass, inline) are processed while saturated + val pending = async { protocol.request (PingRequest()) } + runCurrent() + val outbound = transport.awaitRequest() + transport.deliver(JSONRPCResponse(id = outbound.id, result = EmptyResult())) + runCurrent() + pending.isCompleted shouldBe true + + release.complete(Unit) + advanceUntilIdle() + entered shouldBe 4 + responsesOn(transport) shouldHaveSize 5 + } + + @Test + fun `flood beyond maxInFlightHandlers is rejected fail-fast and the read loop never suspends`() = runTest { + val protocol = TestProtocol( + ProtocolOptions( + handlerCoroutineContext = StandardTestDispatcher(testScheduler), + maxConcurrentHandlers = 1, + maxInFlightHandlers = 4, + ), + ) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val release = CompletableDeferred () + protocol.fallbackRequestHandler = { _, _ -> + release.await() + EmptyResult() + } + + // 10 requests delivered back-to-back; delivery must complete without advancing + // virtual time — i.e. the read loop was never suspended on admission. + val delivery = launch { + repeat(10) { i -> transport.deliver(JSONRPCRequest(id = RequestId(i.toLong()), method = "test/slow")) } + } + runCurrent() + delivery.isCompleted shouldBe true + + // 4 admitted (1 running + 3 parked), 6 rejected immediately + val busyErrors = errorsOn(transport) + busyErrors shouldHaveSize 6 + busyErrors.forEach { + it.error.code shouldBe RPCError.ErrorCode.INTERNAL_ERROR + it.error.message shouldBe "Server is busy: too many in-flight messages" + } + + // ping still answered at full saturation + transport.deliver(JSONRPCRequest(id = RequestId(100L), method = Method.Defined.Ping.value)) + runCurrent() + responsesOn(transport).map { it.id } shouldBe listOf(RequestId(100L)) + + release.complete(Unit) + advanceUntilIdle() + responsesOn(transport) shouldHaveSize 5 // ping + the 4 admitted + } + + @Test + fun `overflowing notifications are dropped and reported via onError`() = runTest { + val protocol = TestProtocol( + ProtocolOptions( + handlerCoroutineContext = StandardTestDispatcher(testScheduler), + maxConcurrentHandlers = 1, + maxInFlightHandlers = 2, + ), + ) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + // parks one handler with a request: holds the sole execution permit and one in-flight slot + val release = CompletableDeferred () + protocol.fallbackRequestHandler = { _, _ -> + release.await() + EmptyResult() + } + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/hog")) + runCurrent() + + // notification 1 is admitted (parked on the semaphore, behind the hogged permit); + // notification 2 overflows the admission cap and is dropped instead of parked. + val notificationRelease = CompletableDeferred () + protocol.fallbackNotificationHandler = { notificationRelease.await() } + transport.deliver(JSONRPCNotification(method = "test/event")) + transport.deliver(JSONRPCNotification(method = "test/event")) + runCurrent() + + protocol.errors shouldHaveSize 1 + protocol.errors.single().message.orEmpty() shouldContain "too many in-flight messages" + errorsOn(transport) shouldBe emptyList() // no wire error for dropped notifications + + release.complete(Unit) + notificationRelease.complete(Unit) + advanceUntilIdle() + } + + @Test + fun `cancelling saturating handlers releases permits and lets parked handlers run`() = runTest { + val protocol = TestProtocol( + ProtocolOptions( + handlerCoroutineContext = StandardTestDispatcher(testScheduler), + maxConcurrentHandlers = 1, + ), + ) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val secondCompleted = CompletableDeferred () + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/hog") { + delay(1000.seconds) // cancellable park + error("unreachable") + } + secondCompleted.complete(Unit) + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/hog")) // holds the permit + transport.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/next")) // parked on the semaphore + runCurrent() + secondCompleted.isCompleted shouldBe false + + // cancel the hog THROUGH the bypass while saturated + transport.deliver(cancelledNotification(requestId = RequestId(1L), reason = "make room")) + advanceUntilIdle() + + secondCompleted.isCompleted shouldBe true + responsesOn(transport).map { it.id } shouldBe listOf(RequestId(2L)) // hog suppressed, next answered + } + + @Test + fun `a parked handler can itself be cancelled before it ever runs`() = runTest { + val protocol = TestProtocol( + ProtocolOptions( + handlerCoroutineContext = StandardTestDispatcher(testScheduler), + maxConcurrentHandlers = 1, + ), + ) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/hog") { + delay(1000.seconds) // cancellable park + error("unreachable") + } + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/hog")) // holds the permit + transport.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/next")) // parked on the semaphore + runCurrent() + + // cancel the parked handler before it ever acquires the permit, then cancel the hog too + transport.deliver(cancelledNotification(requestId = RequestId(2L), reason = "give up while parked")) + transport.deliver(cancelledNotification(requestId = RequestId(1L), reason = "make room")) + advanceUntilIdle() + + // no response for id=2 at all: its withPermit acquire was cancelled before onRequest ever ran + responsesOn(transport) shouldBe emptyList() + errorsOn(transport) shouldBe emptyList() + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptionsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptionsTest.kt new file mode 100644 index 0000000..81bf24a --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOptionsTest.kt @@ -0,0 +1,16 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.Dispatchers +import kotlin.test.Test + +class ProtocolOptionsTest { + + @Test + fun `defaults expose concurrency knobs`() { + val options = ProtocolOptions() + options.handlerCoroutineContext shouldBe Dispatchers.Default + options.maxConcurrentHandlers shouldBe 64 + options.maxInFlightHandlers shouldBe 256 + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOutboundCancellationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOutboundCancellationTest.kt new file mode 100644 index 0000000..38ad556 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolOutboundCancellationTest.kt @@ -0,0 +1,167 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.async +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +class ProtocolOutboundCancellationTest { + + @Test + fun `request times out awaiting the response and sends CancelledNotification with timeout reason`() = runTest { + val protocol = TestProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + val inFlight = async { + shouldThrow { + protocol.request (PingRequest(), RequestOptions(timeout = 5.seconds)) + } + } + val sent = transport.awaitRequest() // request is on the wire; peer never responds + advanceTimeBy(6.seconds) + runCurrent() + + val thrown = inFlight.await() + thrown.code shouldBe RPCError.ErrorCode.REQUEST_TIMEOUT + thrown.data?.jsonObject?.get("timeout")?.jsonPrimitive?.long shouldBe 5.seconds.inWholeMilliseconds + + val cancelledJson = transport.sentWithOptions + .map { it.first } + .filterIsInstance () + .single { it.method == Method.Defined.NotificationsCancelled.value } + val cancelledParams = McpJson.decodeFromJsonElement (cancelledJson.params!!) + cancelledParams.requestId shouldBe sent.id + cancelledParams.reason.shouldNotBeNull() shouldContain "timed out" + } + + @Test + fun `outer withTimeout around request propagates the original timeout exception and notifies the peer`() = runTest { + val protocol = TestProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + val inFlight = async { + shouldThrow { + withTimeout(1.seconds) { + protocol.request (PingRequest()) + } + } + } + transport.awaitRequest() // request is on the wire; peer never responds + advanceTimeBy(2.seconds) + runCurrent() + inFlight.await() + + transport.sentWithOptions.map { it.first }.filterIsInstance () + .filter { it.method == Method.Defined.NotificationsCancelled.value } shouldHaveSize 1 + } + + @Test + fun `caller cancellation sends CancelledNotification and rethrows the original exception`() = runTest { + val protocol = TestProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + val cancelled = CompletableDeferred () + val job = launch { + try { + protocol.request (PingRequest()) + } catch (e: CancellationException) { + cancelled.complete(e) + throw e + } + } + transport.awaitRequest() + job.cancelAndJoin() + + cancelled.await() + val wire = transport.sentWithOptions.map { it.first }.filterIsInstance () + .filter { it.method == Method.Defined.NotificationsCancelled.value } + wire shouldHaveSize 1 + } + + @Test + fun `cancelling the initialize request performs local cleanup but sends no CancelledNotification`() = runTest { + val protocol = TestProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + val job = launch { + protocol.request ( + InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = "t", version = "1"), + ), + ), + ) + } + val sent = transport.awaitRequest() + sent.method shouldBe Method.Defined.Initialize.value + job.cancelAndJoin() + + transport.sentWithOptions.map { it.first }.filterIsInstance () + .filter { it.method == Method.Defined.NotificationsCancelled.value } shouldHaveSize 0 + protocol.responseHandlers shouldBe emptyMap() + } + + @Test + fun `late response for a cancelled request is ignored silently`() = runTest { + val protocol = TestProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + val job = launch { protocol.request (PingRequest()) } + val sent = transport.awaitRequest() + job.cancelAndJoin() + + transport.deliver(JSONRPCResponse(id = sent.id, result = EmptyResult())) + + protocol.errors shouldHaveSize 0 // no "unknown message ID" onError + } + + @Test + fun `response for a genuinely unknown id still reports onError`() = runTest { + val protocol = TestProtocol() + val transport = RecordingTransport() + protocol.connect(transport) + + transport.deliver(JSONRPCResponse(id = RequestId(999L), result = EmptyResult())) + + protocol.errors shouldHaveSize 1 + } +} diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt index b726c2f..2846ccc 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt @@ -5,6 +5,9 @@ import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.types.shouldBeInstanceOf import io.modelcontextprotocol.kotlin.sdk.types.CustomRequest import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage @@ -13,11 +16,16 @@ import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse import io.modelcontextprotocol.kotlin.sdk.types.McpJson import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.PingRequest +import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification +import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.ProgressToken import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.RequestId import io.modelcontextprotocol.kotlin.sdk.types.RequestMeta import kotlinx.coroutines.async -import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObjectBuilder @@ -30,6 +38,7 @@ import kotlinx.serialization.json.jsonPrimitive import kotlin.coroutines.cancellation.CancellationException import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds class ProtocolTest { private lateinit var protocol: TestProtocol @@ -185,64 +194,174 @@ class ProtocolTest { transport.deliver(JSONRPCResponse(sent.id, EmptyResult())) inFlight.await() } -} -private class TestProtocol : Protocol(null) { - val errors = mutableListOf () + @Test + fun `request handler receives enriched extra and ambient context element`() = runTest { + protocol.connect(transport) - override fun onError(error: Throwable) { - errors.add(error) + var seenByParameter: RequestHandlerExtra? = null + var seenAmbient: RequestHandlerExtra? = null + protocol.fallbackRequestHandler = { _, extra -> + seenByParameter = extra + seenAmbient = currentRequestHandlerExtra() + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(7L), method = "custom/echo")) + + seenByParameter shouldNotBe null + seenByParameter?.requestId shouldBe RequestId(7L) + seenByParameter?.method shouldBe Method.Custom("custom/echo") + // the SAME instance flows through both delivery paths + seenAmbient shouldBe seenByParameter } - override fun assertCapabilityForMethod(method: Method) { - // noop - } - override fun assertNotificationCapability(method: Method) { - // noop - } - override fun assertRequestHandlerCapability(method: Method) { - // noop - } -} + @Test + fun `extra method resolves defined methods to Defined entries`() = runTest { + protocol.connect(transport) -private class RecordingTransport : Transport { - private val sentMessages = Channel (Channel.UNLIMITED) - private var onMessageCallback: (suspend (JSONRPCMessage) -> Unit)? = null - private var onCloseCallback: (() -> Unit)? = null + var seenMethod: Method? = null + protocol.setRequestHandler (Method.Defined.Ping) { _, extra -> + seenMethod = extra.method + EmptyResult() + } + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "ping")) - override suspend fun start() { - // noop + seenMethod shouldBe Method.Defined.Ping } - override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { - sentMessages.send(message) + @Test + fun `extra sendNotification stamps relatedRequestId`() = runTest { + protocol.connect(transport) + + protocol.fallbackRequestHandler = { _, extra -> + extra.sendNotification( + ProgressNotification( + ProgressNotificationParams(progressToken = ProgressToken(1L), progress = 0.5), + ), + ) + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(42L), method = "custom/progressing")) + + val sent = transport.sentWithOptions.first { it.first is JSONRPCNotification } + sent.second?.relatedRequestId shouldBe RequestId(42L) } - override suspend fun close() { - onCloseCallback?.invoke() + @Test + fun `extra sendRequest stamps relatedRequestId and preserves caller options`() = runTest { + protocol.connect(transport) + + protocol.fallbackRequestHandler = { _, extra -> + extra.sendRequest ( + PingRequest(), + RequestOptions(resumptionToken = "tok", onProgress = { }, timeout = 30.seconds), + ) + EmptyResult() + } + + // The serial phase runs the handler inline inside deliver(), and the handler suspends + // awaiting the nested request's response — drive delivery from a child coroutine. + val delivery = launch { + transport.deliver(JSONRPCRequest(id = RequestId(42L), method = "custom/nested")) + } + + val outbound = transport.awaitRequest() + val recorded = transport.sentWithOptions + .first { (it.first as? JSONRPCRequest)?.id == outbound.id } + .second + val options = recorded.shouldBeInstanceOf () + options.relatedRequestId shouldBe RequestId(42L) + options.resumptionToken shouldBe "tok" + options.timeout shouldBe 30.seconds + options.onProgress shouldNotBe null + + transport.deliver(JSONRPCResponse(id = outbound.id, result = EmptyResult())) + delivery.join() } - override fun onClose(block: () -> Unit) { - onCloseCallback = block + @Test + fun `currentRequestHandlerExtra returns null outside handlers`() = runTest { + currentRequestHandlerExtra() shouldBe null } - override fun onError(block: (Throwable) -> Unit) { - // noop + @Test + fun `connect while already connected throws IllegalStateException`() = runTest { + protocol.connect(transport) + shouldThrow { + protocol.connect(RecordingTransport()) + } } - override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { - onMessageCallback = block + @Test + fun `stale onClose from a previous transport does not tear down the successor connection`() = runTest { + protocol.connect(transport) + val staleCloseCallback = transport.closeCallback ?: error("onClose not registered") + + protocol.close() // disconnect first transport (fires its own doClose) + val second = RecordingTransport() + protocol.connect(second) // reconnect + + staleCloseCallback() // late duplicate close signal from transport #1 + + // successor connection must still be alive + protocol.transport shouldBe second } - suspend fun awaitRequest(): JSONRPCRequest { - val message = sentMessages.receive() - return message as? JSONRPCRequest - ?: error("Expected JSONRPCRequest but received ${message::class.simpleName}") + @Test + fun `failed transport start rolls back the connection and allows reconnect`() = runTest { + val failing = object : Transport { + override suspend fun start(): Unit = error("boom") + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) {} + override suspend fun close() {} + override fun onClose(block: () -> Unit) {} + override fun onError(block: (Throwable) -> Unit) {} + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) {} + } + shouldThrow { protocol.connect(failing) } + protocol.transport shouldBe null // rolled back + protocol.connect(transport) // reconnect succeeds, no "already connected" + protocol.transport shouldBe transport } - suspend fun deliver(message: JSONRPCMessage) { - val callback = onMessageCallback ?: error("onMessage callback not registered") - callback(message) + @Test + fun `request cleans up its handlers when the transport send fails`() = runTest { + // Send fails without closing the connection, so the leak is not masked by doClose(). + val failingSend = object : Transport { + override suspend fun start() {} + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?): Unit = + throw IllegalStateException("send failed") + override suspend fun close() {} + override fun onClose(block: () -> Unit) {} + override fun onError(block: (Throwable) -> Unit) {} + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) {} + } + protocol.connect(failingSend) + + shouldThrow { + protocol.request (PingRequest(), RequestOptions(onProgress = {})) + } + + protocol.responseHandlers.size shouldBe 0 + protocol.progressHandlers.size shouldBe 0 + } + + @Test + fun `connect rejects non-positive maxConcurrentHandlers`() = runTest { + val error = shouldThrow { + TestProtocol(ProtocolOptions(maxConcurrentHandlers = 0)).connect(RecordingTransport()) + } + error.message.shouldNotBeNull() shouldContain "maxConcurrentHandlers" + } + + @Test + fun `connect rejects maxInFlightHandlers below maxConcurrentHandlers`() = runTest { + val error = shouldThrow { + TestProtocol(ProtocolOptions(maxConcurrentHandlers = 4, maxInFlightHandlers = 0)) + .connect(RecordingTransport()) + } + error.message.shouldNotBeNull() shouldContain "maxInFlightHandlers" } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTestFakes.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTestFakes.kt new file mode 100644 index 0000000..9e8dd60 --- /dev/null +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTestFakes.kt @@ -0,0 +1,112 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import kotlinx.coroutines.channels.Channel +import kotlinx.serialization.json.encodeToJsonElement + +/** + * Shared [Protocol] test double: records [onError] calls, counts `notifications/initialized` + * router-hook invocations, and no-ops all capability assertions. + * + * The concurrency gate is flipped only via [enableConcurrency]; nothing in this double flips it + * implicitly, so tests keep today's serial semantics unless they opt in. + */ +internal class TestProtocol(options: ProtocolOptions? = null) : Protocol(options) { + val errors = mutableListOf () + var initializedNotificationCount = 0 + + fun enableConcurrency() { + enableConcurrentDispatch() + } + + override fun onInitializedNotification() { + initializedNotificationCount++ + // deliberately does NOT flip the gate — tests control the flip explicitly + } + + override fun onError(error: Throwable) { + errors.add(error) + } + + override fun assertCapabilityForMethod(method: Method) { + // noop + } + override fun assertNotificationCapability(method: Method) { + // noop + } + public override fun assertRequestHandlerCapability(method: Method) { + // noop + } +} + +/** Shared [Transport] test double: records every sent message (with its options) and replays inbound ones. */ +internal class RecordingTransport : Transport { + val sentMessages = Channel (Channel.UNLIMITED) + private var onMessageCallback: (suspend (JSONRPCMessage) -> Unit)? = null + private var onCloseCallback: (() -> Unit)? = null + + val sentWithOptions = mutableListOf >() + + var closeCallback: (() -> Unit)? = null + private set + + override suspend fun start() { + // noop + } + + override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { + sentWithOptions.add(message to options) + sentMessages.send(message) + } + + override suspend fun close() { + onCloseCallback?.invoke() + } + + override fun onClose(block: () -> Unit) { + closeCallback = block + onCloseCallback = block + } + + override fun onError(block: (Throwable) -> Unit) { + // noop + } + + override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { + onMessageCallback = block + } + + suspend fun awaitRequest(): JSONRPCRequest { + while (true) { + val message = sentMessages.receive() + if (message is JSONRPCRequest) return message + } + } + + /** Cross-thread-safe reader used by real-dispatcher tests; [sentWithOptions] is single-thread only. */ + suspend fun awaitSent(): JSONRPCMessage = sentMessages.receive() + + suspend fun deliver(message: JSONRPCMessage) { + val callback = onMessageCallback ?: error("onMessage callback not registered") + callback(message) + } +} + +internal fun responsesOn(transport: RecordingTransport): List = + transport.sentWithOptions.map { it.first }.filterIsInstance () + +internal fun errorsOn(transport: RecordingTransport): List = + transport.sentWithOptions.map { it.first }.filterIsInstance () + +internal fun cancelledNotification(requestId: RequestId, reason: String): JSONRPCNotification = JSONRPCNotification( + method = Method.Defined.NotificationsCancelled.value, + params = McpJson.encodeToJsonElement(CancelledNotificationParams(requestId = requestId, reason = reason)), +) diff --git a/kotlin-sdk-core/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyJvmTest.kt b/kotlin-sdk-core/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyJvmTest.kt new file mode 100644 index 0000000..60658f8 --- /dev/null +++ b/kotlin-sdk-core/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolConcurrencyJvmTest.kt @@ -0,0 +1,114 @@ +package io.modelcontextprotocol.kotlin.sdk.shared + +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse +import io.modelcontextprotocol.kotlin.sdk.types.RequestId +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +/** + * Real-dispatcher counterparts to [ProtocolConcurrencyTest]. + * + * These run under [runBlocking] (real time, real threads on [Dispatchers.Default]) rather than + * `runTest`: a virtual-time scheduler auto-advances past the `withTimeout` guards and cannot + * observe genuine cross-thread parallelism, so the concurrency assertions could pass vacuously. + */ +class ProtocolConcurrencyJvmTest { + + @Test + fun `fast request completes while a slow handler is parked on Dispatchers Default`() = runBlocking { + val protocol = TestProtocol(ProtocolOptions(handlerCoroutineContext = Dispatchers.Default)) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + val slowEntered = CompletableDeferred () + val releaseSlow = CompletableDeferred () + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/slow") { + slowEntered.complete(Unit) + releaseSlow.await() + } + EmptyResult() + } + + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/slow")) + withTimeout(5.seconds) { slowEntered.await() } + transport.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/fast")) + + val fast = withTimeout(5.seconds) { transport.awaitSent() } + (fast as JSONRPCResponse).id shouldBe RequestId(2L) + + releaseSlow.complete(Unit) + val slow = withTimeout(5.seconds) { transport.awaitSent() } + (slow as JSONRPCResponse).id shouldBe RequestId(1L) + Unit + } + + @Test + fun `dispatch stays serial before the gate flips on Dispatchers Default`() = runBlocking { + val protocol = TestProtocol(ProtocolOptions(handlerCoroutineContext = Dispatchers.Default)) + val transport = RecordingTransport() + protocol.connect(transport) + // gate NOT flipped + + val slowEntered = CompletableDeferred () + val releaseSlow = CompletableDeferred () + protocol.fallbackRequestHandler = { request, _ -> + if (request.method == "test/slow") { + slowEntered.complete(Unit) + releaseSlow.await() + } + EmptyResult() + } + + // A single delivering coroutine models the transport read loop: the second deliver cannot + // start until the first returns, so a parked slow handler blocks the fast request. + val delivery = launch { + transport.deliver(JSONRPCRequest(id = RequestId(1L), method = "test/slow")) + transport.deliver(JSONRPCRequest(id = RequestId(2L), method = "test/fast")) + } + withTimeout(5.seconds) { slowEntered.await() } + + // The delivering coroutine is parked inside the slow handler; fast was never delivered. + transport.sentMessages.tryReceive().isFailure shouldBe true + + releaseSlow.complete(Unit) + val first = withTimeout(5.seconds) { transport.awaitSent() } + (first as JSONRPCResponse).id shouldBe RequestId(1L) + val second = withTimeout(5.seconds) { transport.awaitSent() } + (second as JSONRPCResponse).id shouldBe RequestId(2L) + delivery.join() + } + + @Test + fun `suspension-free notification handlers keep arrival order on Dispatchers Default`() = runBlocking { + val protocol = TestProtocol(ProtocolOptions(handlerCoroutineContext = Dispatchers.Default)) + val transport = RecordingTransport() + protocol.connect(transport) + protocol.enableConcurrency() + + // safe: appended only from the delivering coroutine (UNDISPATCHED, suspension-free) + val seen = mutableListOf () + protocol.fallbackNotificationHandler = { notification -> + seen.add((notification.params as JsonObject).getValue("i").jsonPrimitive.int) + } + repeat(100) { i -> + transport.deliver(JSONRPCNotification(method = "test/event", params = buildJsonObject { put("i", i) })) + } + seen shouldBe (0 until 100).toList() + } +} diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index 6094b7c..51ac279 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -169,8 +169,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/ServerOptions : io/ public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;)V public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Z)V public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;ZLio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory;)V - public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;ZLio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;ZLio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory;Lkotlin/coroutines/CoroutineContext;)V + public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;ZLio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities; public final fun getResourceTemplateMatcherFactory ()Lio/modelcontextprotocol/kotlin/sdk/utils/ResourceTemplateMatcherFactory; } @@ -198,6 +198,7 @@ public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelc public fun onClose ()V public final fun onClose (Lkotlin/jvm/functions/Function0;)V public final fun onInitialized (Lkotlin/jvm/functions/Function0;)V + protected fun onInitializedNotification ()V public final fun ping (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun sendElicitationComplete (Lio/modelcontextprotocol/kotlin/sdk/types/ElicitationCompleteNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun sendLoggingMessage (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingMessageNotification;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt index c0d3e84..51a2389 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Server.kt @@ -53,9 +53,11 @@ import io.modelcontextprotocol.kotlin.sdk.utils.ResourceTemplateMatcher import io.modelcontextprotocol.kotlin.sdk.utils.ResourceTemplateMatcherFactory import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put +import kotlin.coroutines.CoroutineContext import kotlin.jvm.JvmOverloads import kotlin.time.ExperimentalTime @@ -69,12 +71,17 @@ private val logger = KotlinLogging.logger {} * Forwarded to [ProtocolOptions.enforceStrictCapabilities]. * @property resourceTemplateMatcherFactory The factory used to create [ResourceTemplateMatcher] instances * for matching resource URIs against registered templates. Defaults to [PathSegmentTemplateMatcher.factory]. + * @param handlerCoroutineContext Coroutine context for inbound handlers. See [ProtocolOptions.handlerCoroutineContext]. */ public class ServerOptions( public val capabilities: ServerCapabilities, enforceStrictCapabilities: Boolean = true, public val resourceTemplateMatcherFactory: ResourceTemplateMatcherFactory = PathSegmentTemplateMatcher.factory, -) : ProtocolOptions(enforceStrictCapabilities = enforceStrictCapabilities) { + handlerCoroutineContext: CoroutineContext = Dispatchers.Default, +) : ProtocolOptions( + enforceStrictCapabilities = enforceStrictCapabilities, + handlerCoroutineContext = handlerCoroutineContext, +) { @JvmOverloads public constructor( capabilities: ServerCapabilities, diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt index d215bd5..880f1cf 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt @@ -108,10 +108,18 @@ public open class ServerSession( } } + override fun onInitializedNotification() { + // Enable concurrency before the user's onInitialized callback, which runs synchronously. + enableConcurrentDispatch() + } + internal val clientConnection: ClientConnection = ClientConnectionImpl(this) /** * Registers a callback to be invoked when the server has completed initialization. + * + * The callback must be synchronous and fast: it runs on the message-dispatch path for + * `notifications/initialized`, after concurrent dispatch has been enabled for the session. */ public fun onInitialized(block: () -> Unit) { val old = _onInitialized diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index c213350..1f8ac39 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -15,10 +15,12 @@ import io.ktor.server.sse.ServerSSESession import io.ktor.util.collections.ConcurrentMap import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions +import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams import io.modelcontextprotocol.kotlin.sdk.types.DEFAULT_NEGOTIATED_PROTOCOL_VERSION import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCEmptyMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse import io.modelcontextprotocol.kotlin.sdk.types.McpJson @@ -27,6 +29,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.RPCError import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.REQUEST_TIMEOUT import io.modelcontextprotocol.kotlin.sdk.types.RequestId import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.job @@ -175,6 +178,20 @@ public class StreamableHttpServerTransport(private val configuration: Configurat private val requestToStreamMapping: ConcurrentMap = ConcurrentMap() private val requestToResponseMapping: ConcurrentMap = ConcurrentMap() + /** + * JSON-response mode only: one entry per in-flight POST, completed by [send] with the collected + * responses once every request delivered by that POST has been answered. [handlePostRequest] + * awaits it so the HTTP exchange stays open while handlers run concurrently. + */ + private val pendingJsonResponses: ConcurrentMap >> = ConcurrentMap() + + /** + * JSON-response mode only: request ids retired by an inbound cancellation while their POST is + * still awaiting. Treated as settled-without-response so the POST's "a response for every id" + * completion condition is not blocked by a request that will never be answered. + */ + private val cancelledRequestIds: ConcurrentMap = ConcurrentMap() + private val sessionMutex = Mutex() private val streamMutex = Mutex() @@ -241,54 +258,117 @@ public class StreamableHttpServerTransport(private val configuration: Configurat return } + val isTerminated = message is JSONRPCResponse || message is JSONRPCError + + // In JSON mode request-related notifications have no per-request SSE stream. Route them to the + // standalone GET stream before the lookup below, so a notification for an already-retired + // request is delivered there instead of becoming a spurious error. + if (configuration.enableJsonResponse && !isTerminated) { + val standaloneStream = streamsMapping[STANDALONE_SSE_STREAM_ID] + standaloneStream?.let { emitOnStream(STANDALONE_SSE_STREAM_ID, it.session, message) } + return + } + val streamId = requestToStreamMapping[routingRequestId] - ?: error("No connection established for request id $routingRequestId") + if (streamId == null) { + // Request already retired (completed, cancelled, or the JSON POST disconnected). In JSON + // mode a late terminal response is undeliverable and dropped. In SSE mode an unknown id + // is a real routing error. + if (configuration.enableJsonResponse) return + error("No connection established for request id $routingRequestId") + } val activeStream = streamsMapping[streamId] if (!configuration.enableJsonResponse) { activeStream?.let { stream -> emitOnStream(streamId, stream.session, message) } - } - - val isTerminated = message is JSONRPCResponse || message is JSONRPCError - if (!isTerminated) { - if (configuration.enableJsonResponse) { - // In JSON response mode there is no per-request SSE stream, so route notifications - // that are logically associated with a request to the standalone GET SSE stream. - val standaloneStream = streamsMapping[STANDALONE_SSE_STREAM_ID] - standaloneStream?.let { emitOnStream(STANDALONE_SSE_STREAM_ID, it.session, message) } - } - return + if (!isTerminated) return } requestToResponseMapping[responseRequestId!!] = message - val relatedIds = requestToStreamMapping.filterValues { it == streamId }.keys - - if (relatedIds.any { it !in requestToResponseMapping }) return streamMutex.withLock { - if (activeStream == null) error("No connection established for request ID: $routingRequestId") + // The request may have been retired (disconnect or cancellation cleanup) between recording + // the response above and acquiring the lock. Reclaim the stray mapping and drop it. + if (routingRequestId !in requestToStreamMapping) { + requestToResponseMapping.remove(routingRequestId) + return + } + // Recomputed under the lock. Handlers may respond concurrently, and the sender that + // observes the last settled request also retires the ids, so a losing sender sees an + // empty set. A cancelled request counts as settled even though it produces no response. + val relatedIds = requestToStreamMapping.filterValues { it == streamId }.keys + if (relatedIds.isEmpty() || + relatedIds.any { it !in requestToResponseMapping && it !in cancelledRequestIds } + ) { + return + } if (configuration.enableJsonResponse) { - activeStream.call.response.header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) - sessionId?.let { activeStream.call.response.header(MCP_SESSION_ID_HEADER, it) } - val responses = relatedIds.mapNotNull { requestToResponseMapping[it] } - val payload = if (responses.size == 1) { - responses.first() - } else { - responses + // Hand the collected responses to the POST suspended in [handlePostRequest], which + // completes the HTTP exchange. A null pendingPost means the POST was already retired + // (disconnect or cancellation), so the responses are undeliverable and dropped quietly. + val pendingPost = pendingJsonResponses.remove(streamId) + if (pendingPost == null) { + cleanUpRequests(relatedIds) + return } - activeStream.call.respond(payload) + val responses = relatedIds.mapNotNull { requestToResponseMapping[it] } + cleanUpRequests(relatedIds) + pendingPost.complete(responses) } else { - activeStream.session?.close() + // A null stream means the per-request SSE connection closed before the response + // settled, so the ids are retired rather than leaked. + activeStream?.session?.close() + cleanUpRequests(relatedIds) } + } + } - // Clean up - relatedIds.forEach { requestId -> - requestToResponseMapping.remove(requestId) - requestToStreamMapping.remove(requestId) + private fun cleanUpRequests(ids: Collection ) { + ids.forEach { requestId -> + requestToResponseMapping.remove(requestId) + requestToStreamMapping.remove(requestId) + cancelledRequestIds.remove(requestId) + } + } + + /** + * Scans a delivered POST body for `notifications/cancelled` and retires each referenced request + * that is awaiting on a JSON-mode POST. No-op unless [Configuration.enableJsonResponse]. + */ + private suspend fun retireCancelledJsonRequests(messages: List ) { + if (!configuration.enableJsonResponse) return + val cancelledIds = messages.asSequence() + .filterIsInstance () + .filter { it.method == Method.Defined.NotificationsCancelled.value } + .mapNotNull { notification -> + val params = notification.params ?: return@mapNotNull null + runCatching { + McpJson.decodeFromJsonElement (params).requestId + }.getOrNull() } + .toList() + cancelledIds.forEach { retireCancelledJsonRequest(it) } + } + + /** + * Marks a request cancelled by the peer as settled without a response and completes its POST once + * every request that POST delivered is settled (answered or cancelled). Without this, a cancelled + * request leaves the POST awaiting a response that, by design, is never sent. + */ + private suspend fun retireCancelledJsonRequest(requestId: RequestId) { + streamMutex.withLock { + val streamId = requestToStreamMapping[requestId] ?: return + cancelledRequestIds[requestId] = Unit + val relatedIds = requestToStreamMapping.filterValues { it == streamId }.keys + val allSettled = relatedIds.all { it in requestToResponseMapping || it in cancelledRequestIds } + if (!allSettled) return + val pendingPost = pendingJsonResponses.remove(streamId) ?: return + val responses = relatedIds.mapNotNull { requestToResponseMapping[it] } + cleanUpRequests(relatedIds) + pendingPost.complete(responses) } } @@ -304,6 +384,10 @@ public class StreamableHttpServerTransport(private val configuration: Configurat streamsMapping.clear() requestToStreamMapping.clear() requestToResponseMapping.clear() + cancelledRequestIds.clear() + // Release any POST calls suspended awaiting JSON responses. + pendingJsonResponses.values.forEach { it.cancel() } + pendingJsonResponses.clear() invokeOnCloseCallback() } } @@ -339,7 +423,12 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } /** - * Handles POST requests containing JSON-RPC messages + * Handles POST requests containing JSON-RPC messages. + * + * In JSON-response mode this suspends until a response has been produced for every request in + * the POST body, then completes the HTTP exchange with the collected JSON (a single object for + * one request, an array for a batch). Bodies without requests are acknowledged with 202 + * immediately. In SSE mode the responses are emitted onto the per-request SSE stream instead. */ public suspend fun handlePostRequest(session: ServerSSESession?, call: ApplicationCall) { try { @@ -407,6 +496,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat if (!hasRequest) { call.respondNullable(status = HttpStatusCode.Accepted, message = null) messages.forEach { message -> _onMessage(message) } + // A cancellation may target a request still awaiting delivery on a JSON-mode POST; + // retire it so that POST completes instead of waiting for a response that never comes. + retireCancelledJsonRequests(messages) return } @@ -429,13 +521,50 @@ public class StreamableHttpServerTransport(private val configuration: Configurat maybeSendPrimingEvent(streamId, session, clientProtocolVersion) } + val pendingPost = if (configuration.enableJsonResponse) { + CompletableDeferred >() + } else { + null + } + streamMutex.withLock { streamsMapping[streamId] = SessionContext(session, call) messages.filterIsInstance
().forEach { requestToStreamMapping[it.id] = streamId } + pendingPost?.let { pendingJsonResponses[streamId] = it } } call.coroutineContext.job.invokeOnCompletion { streamsMapping.remove(streamId) } - messages.forEach { message -> _onMessage(message) } + if (pendingPost == null) { + // SSE mode: responses are emitted onto this call's SSE stream by send(). + messages.forEach { message -> _onMessage(message) } + return + } + + // JSON-response mode. Handlers may run concurrently, so returning from delivery no longer + // implies the responses were produced. Suspend until send() has collected a response for + // every request in this POST. A client disconnect or transport close cancels this call. + try { + messages.forEach { message -> _onMessage(message) } + retireCancelledJsonRequests(messages) + val responses = pendingPost.await() + sessionId?.let { call.response.header(MCP_SESSION_ID_HEADER, it) } + if (responses.isEmpty()) { + // Every request in this POST was cancelled before producing a response; there is + // nothing to return, so acknowledge with 202 rather than an empty JSON body. + call.respondNullable(status = HttpStatusCode.Accepted, message = null) + } else { + call.response.header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + val payload = if (responses.size == 1) responses.first() else responses + call.respond(payload) + } + } finally { + // On cancellation or failure, retire this POST's ids so a late response is dropped + // quietly at the send() boundary instead of leaking mappings. Idempotent no-ops on + // the success path (send() or retirement already cleaned up). + pendingJsonResponses.remove(streamId) + pendingPost.cancel() + cleanUpRequests(messages.filterIsInstance ().map { it.id }) + } } catch (e: CancellationException) { throw e } catch (e: Exception) { diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt index d7dec4e..172889a 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt @@ -19,14 +19,19 @@ import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.ApplicationCall import io.ktor.server.application.install import io.ktor.server.routing.post import io.ktor.server.routing.routing +import io.ktor.server.sse.ServerSSESession import io.ktor.server.testing.ApplicationTestBuilder import io.ktor.server.testing.testApplication +import io.ktor.sse.ServerSentEvent import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.readLine import io.ktor.utils.io.readUTF8Line +import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotification +import io.modelcontextprotocol.kotlin.sdk.types.CancelledNotificationParams import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation @@ -46,6 +51,16 @@ import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.Tool import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put @@ -53,11 +68,13 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.CoroutineContext import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertNotNull +import kotlin.time.Duration.Companion.seconds import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation @@ -349,6 +366,306 @@ class StreamableHttpServerTransportTest { assertEquals("second", secondMeta?.get("label")?.jsonPrimitive?.content)*/ } + @Test + fun `json response waits for a response produced after message delivery returns`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration(enableJsonResponse = true), + ) + // Simulates concurrent dispatch: message delivery returns before the handler responds. + val handlerScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + try { + transport.onMessage { message -> + if (message is JSONRPCRequest) { + if (message.method == Method.Defined.Initialize.value) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } else { + handlerScope.launch { + delay(100) + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + } + } + + configureTransportEndpoint(transport) + + val initResponse = client.post(path) { + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + + val request = JSONRPCRequest(id = RequestId("async-1"), method = Method.Defined.ToolsList.value) + val response = withTimeout(10.seconds) { + client.post(path) { + addStreamableHeaders() + header(MCP_SESSION_ID_HEADER, initResponse.headers[MCP_SESSION_ID_HEADER]) + setBody(encodeMessages(listOf(request))) + } + } + + response.status shouldBe HttpStatusCode.OK + response.body () shouldBe JSONRPCResponse(request.id) + } finally { + handlerScope.cancel() + } + } + + @Test + fun `json response for a batch waits until every asynchronously produced response is ready`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration(enableJsonResponse = true), + ) + val firstRequest = JSONRPCRequest(id = RequestId("slow"), method = Method.Defined.ToolsList.value) + val secondRequest = JSONRPCRequest(id = RequestId("fast"), method = Method.Defined.ResourcesList.value) + + // Simulates concurrent dispatch with out-of-order completion: the second request responds + // first; the POST must stay open until the slower first response settles too. + val handlerScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + try { + transport.onMessage { message -> + if (message is JSONRPCRequest) { + if (message.method == Method.Defined.Initialize.value) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } else { + handlerScope.launch { + delay(if (message.id == firstRequest.id) 200 else 50) + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } + } + } + } + + configureTransportEndpoint(transport) + + val initResponse = client.post(path) { + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + + val response = withTimeout(10.seconds) { + client.post(path) { + addStreamableHeaders() + header(MCP_SESSION_ID_HEADER, initResponse.headers[MCP_SESSION_ID_HEADER]) + setBody(encodeMessages(listOf(firstRequest, secondRequest))) + } + } + + response.status shouldBe HttpStatusCode.OK + val responses = response.body >() + responses.map { it.id }.toSet() shouldBe setOf(firstRequest.id, secondRequest.id) + } finally { + handlerScope.cancel() + } + } + + @Test + fun `json response POST completes with 202 when its only request is cancelled`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration(enableJsonResponse = true), + ) + val requestDelivered = CompletableDeferred
() + // Concurrent dispatch: delivery returns before a response is produced. This request is never + // answered here, modelling a handler cancelled by notifications/cancelled. + transport.onMessage { message -> + if (message is JSONRPCRequest) { + if (message.method == Method.Defined.Initialize.value) { + transport.send(JSONRPCResponse(message.id, EmptyResult())) + } else { + requestDelivered.complete(Unit) + } + } + } + + configureTransportEndpoint(transport) + + val clientScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + try { + val initResponse = client.post(path) { + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = initResponse.headers[MCP_SESSION_ID_HEADER] + + val request = JSONRPCRequest(id = RequestId("cancel-me"), method = Method.Defined.ToolsList.value) + val requestPost = clientScope.async { + client.post(path) { + addStreamableHeaders() + header(MCP_SESSION_ID_HEADER, sessionId) + setBody(encodeMessages(listOf(request))) + } + } + + // The request must be registered in-flight before it can be cancelled. + withTimeout(5.seconds) { requestDelivered.await() } + + val cancellation = CancelledNotification( + CancelledNotificationParams(requestId = request.id, reason = "client cancelled"), + ) + val cancelResponse = client.post(path) { + addStreamableHeaders() + header(MCP_SESSION_ID_HEADER, sessionId) + setBody(encodeMessages(listOf(cancellation.toJSON()))) + } + cancelResponse.status shouldBe HttpStatusCode.Accepted + + // Without retirement of the cancelled id this await never completes and the timeout fires. + val response = withTimeout(10.seconds) { requestPost.await() } + response.status shouldBe HttpStatusCode.Accepted + } finally { + clientScope.cancel() + } + } + + @Test + fun `json response for a batch returns only the non-cancelled responses`() = testApplication { + configTestServer() + + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration(enableJsonResponse = true), + ) + val answeredA = JSONRPCRequest(id = RequestId("a"), method = Method.Defined.ToolsList.value) + val answeredB = JSONRPCRequest(id = RequestId("b"), method = Method.Defined.ToolsList.value) + val cancelledC = JSONRPCRequest(id = RequestId("c"), method = Method.Defined.ToolsList.value) + + val cancelledDelivered = CompletableDeferred () + val handlerScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + try { + transport.onMessage { message -> + if (message is JSONRPCRequest) { + when { + message.method == Method.Defined.Initialize.value -> + transport.send(JSONRPCResponse(message.id, EmptyResult())) + + message.id == answeredA.id || message.id == answeredB.id -> + handlerScope.launch { transport.send(JSONRPCResponse(message.id, EmptyResult())) } + + message.id == cancelledC.id -> cancelledDelivered.complete(Unit) + } + } + } + + configureTransportEndpoint(transport) + + val initResponse = client.post(path) { + addStreamableHeaders() + setBody(buildInitializeRequestPayload()) + } + initResponse.status shouldBe HttpStatusCode.OK + val sessionId = initResponse.headers[MCP_SESSION_ID_HEADER] + + val batchPost = handlerScope.async { + client.post(path) { + addStreamableHeaders() + header(MCP_SESSION_ID_HEADER, sessionId) + setBody(encodeMessages(listOf(answeredA, answeredB, cancelledC))) + } + } + + withTimeout(5.seconds) { cancelledDelivered.await() } + + val cancellation = CancelledNotification(CancelledNotificationParams(requestId = cancelledC.id)) + client.post(path) { + addStreamableHeaders() + header(MCP_SESSION_ID_HEADER, sessionId) + setBody(encodeMessages(listOf(cancellation.toJSON()))) + }.status shouldBe HttpStatusCode.Accepted + + val response = withTimeout(10.seconds) { batchPost.await() } + response.status shouldBe HttpStatusCode.OK + val responses = response.body >() + responses.map { it.id }.toSet() shouldBe setOf(answeredA.id, answeredB.id) + } finally { + handlerScope.cancel() + } + } + + @Test + fun `json response send for a retired request id is dropped without error`() = runTest { + val transport = StreamableHttpServerTransport(enableJsonResponse = true) + // No stream is mapped for this id (already retired by disconnect/cancellation cleanup); + // a late terminal response must be dropped quietly, not raise "No connection established". + transport.send(JSONRPCResponse(RequestId("retired"), EmptyResult())) + } + + @Test + fun `sse response send for an unknown request id still errors`() = runTest { + val transport = StreamableHttpServerTransport(enableJsonResponse = false) + assertFailsWith
{ + transport.send(JSONRPCResponse(RequestId("unknown"), EmptyResult())) + } + } + + @Test + fun `sse terminal response is dropped without leaking when the per-request stream is gone`() = testApplication { + configTestServer() + val client = createTestClient() + + val transport = StreamableHttpServerTransport( + StreamableHttpServerTransport.Configuration(enableJsonResponse = false), + ) + transport.setSessionIdGenerator(null) // stateless: accept requests without an init handshake + val requestId = RequestId("held-request") + val delivered = CompletableDeferred () + val release = CompletableDeferred () + transport.onMessage { message -> + if (message is JSONRPCRequest) { + delivered.complete(Unit) + release.await() // keep the POST and its stream mapping alive until the test releases it + } + } + + application { + routing { + post(path) { + transport.handlePostRequest(FakeServerSSESession(call, call.coroutineContext), call) + } + } + } + + val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + try { + val post = scope.async { + client.post(path) { + addStreamableHeaders() + setBody( + encodeMessages( + listOf(JSONRPCRequest(id = requestId, method = Method.Defined.ToolsList.value)), + ), + ) + } + } + withTimeout(5.seconds) { delivered.await() } + + // Drop the per-request SSE stream while the request is still mapped in-flight; the handler's + // terminal response is now undeliverable and must be dropped quietly rather than raise + // "No connection established" (which would also strand the request mappings). + transport.closeSseStream(requestId) + transport.send(JSONRPCResponse(requestId, EmptyResult())) + + post.cancel() + } finally { + release.complete(Unit) + scope.cancel() + } + } + @ParameterizedTest @MethodSource("invalidPayloads") fun `POST with a null or empty body returns an error`(payload: String?) = testApplication { @@ -694,3 +1011,12 @@ class StreamableHttpServerTransportTest { } } } + +/** No-op [ServerSSESession] double that lets a POST route drive [StreamableHttpServerTransport] in SSE mode. */ +private class FakeServerSSESession( + override val call: ApplicationCall, + override val coroutineContext: CoroutineContext, +) : ServerSSESession { + override suspend fun send(event: ServerSentEvent) {} + override suspend fun close() {} +} From 76987a211b08e0d42f688f7e038eeb449a9ab385 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:21:41 +0200 Subject: [PATCH 024/106] chore(deps): bump the all-dependencies group across 1 directory with 3 updates (#891) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-dependencies group with 3 updates in the /integration-test/src/jvmTest/typescript directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [tsx](https://github.com/privatenumber/tsx) and [typescript](https://github.com/microsoft/TypeScript). Updates `@types/node` from 26.1.0 to 26.1.1 Commits
- See full diff in compare view
Updates `tsx` from 4.23.0 to 4.23.1Release notes
Sourced from tsx's releases.
v4.23.1
4.23.1 (2026-07-13)
Bug Fixes
- support tsImport after global preload (8d4ffc2)
- watch: avoid clearing piped output (95d0672)
- watch: treat script and dependency paths literally (79fddde)
Performance Improvements
- index transform cache lazily (e818ad6)
- load esbuild lazily in CLI (d067938)
- map Node TypeScript formats directly (cdcc623)
- use sync module hooks on Node v22.22.3+ (f8992f1)
This release is also available on:
Commits
79fdddefix(watch): treat script and dependency paths literallye818ad6perf: index transform cache lazilycdcc623perf: map Node TypeScript formats directlyd067938perf: load esbuild lazily in CLI95d0672fix(watch): avoid clearing piped output6fd4607docs: add per-page metadataf4176d8docs: generate sitemap8d4ffc2fix: support tsImport after global preloadf0e89b2docs: document Node's public type-stripping API vs internal loader pathf8992f1perf: use sync module hooks on Node v22.22.3+- See full diff in compare view
Updates `typescript` from 6.0.3 to 7.0.2Commits
- See full diff in compare view
Maintainer changes
This version was pushed to npm by microsoft1es, a new releaser for typescript since your current version.
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/jvmTest/typescript/package-lock.json | 391 +++++++++++++++++- .../src/jvmTest/typescript/package.json | 6 +- 2 files changed, 379 insertions(+), 18 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 4d0c500..c9d73bf 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -15,10 +15,10 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^26.1.0", + "@types/node": "^26.1.1", "cors": "^2.8.6", - "tsx": "^4.23.0", - "typescript": "^6.0.3" + "tsx": "^4.23.1", + "typescript": "^7.0.2" } }, "node_modules/@esbuild/aix-ppc64": { @@ -579,9 +579,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", - "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", "dev": true, "license": "MIT", "dependencies": { @@ -623,6 +623,346 @@ "@types/node": "*" } }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1680,9 +2020,9 @@ } }, "node_modules/tsx": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", - "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1713,17 +2053,38 @@ } }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "dev": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "tsc": "bin/tsc" }, "engines": { - "node": ">=14.17" + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" } }, "node_modules/undici-types": { diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json index bf81140..0cbee82 100644 --- a/integration-test/src/jvmTest/typescript/package.json +++ b/integration-test/src/jvmTest/typescript/package.json @@ -16,9 +16,9 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^26.1.0", + "@types/node": "^26.1.1", "cors": "^2.8.6", - "tsx": "^4.23.0", - "typescript": "^6.0.3" + "tsx": "^4.23.1", + "typescript": "^7.0.2" } } From 8f104268c787e3af2e37b625d45b08b80299d429 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:51:27 +0200 Subject: [PATCH 025/106] chore(deps): bump the kotest group across 1 directory with 3 updates (#888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the kotest group with 3 updates in the / directory: [io.kotest:kotest-assertions-core](https://github.com/kotest/kotest), [io.kotest:kotest-assertions-json](https://github.com/kotest/kotest) and [io.kotest:kotest-assertions-ktor](https://github.com/kotest/kotest). Updates `io.kotest:kotest-assertions-core` from 6.2.1 to 6.2.2 Release notes
Sourced from io.kotest:kotest-assertions-core's releases.
v6.2.2
What's Changed
- Document instant-based ZonedDateTime
plusOrMinuscomparison by@AlexCue987with@Copilotin kotest/kotest#6159- Add documentation for OffsetDateTime tolerance matcher by
@AlexCue987in kotest/kotest#6161- Fix typo in proptest config docs by
@axiomin kotest/kotest#6155- disable return-value-checker for releases by
@alfonsoristoratoin kotest/kotest#6165- fix(kotest-framework-engine): Use display-name rendering for ignored tests in TeamCity listener by
@sksamuelin kotest/kotest#6119- fix(kotest-assertions-core): Handle null map values in shouldBeEqualUsingFields by
@sksamuelin kotest/kotest#6112- Update dependency io.mockk:mockk to v1.14.11 by
@renovate[bot] in kotest/kotest#6163- fix: attach Fail fast scope tracker for each root test case in instance per spec mode by
@ashdawngaryin kotest/kotest#6160- Update dependency io.projectreactor.tools:blockhound to v1.0.17.RELEASE by
@renovate[bot] in kotest/kotest#6164New Contributors
@AlexCue987with@Copilotmade their first contribution in kotest/kotest#6159@axiommade their first contribution in kotest/kotest#6155@ashdawngarymade their first contribution in kotest/kotest#6160Full Changelog: https://github.com/kotest/kotest/compare/v6.2.1...v6.2.2
Commits
4e55e4cUpdate dependency io.projectreactor.tools:blockhound to v1.0.17.RELEASE (#6164)3e0202ffix: attach Fail fast scope tracker for each root test case in instance per s...134c12bUpdate dependency io.mockk:mockk to v1.14.11 (#6163)01c7429fix(kotest-assertions-core): Handle null map values in shouldBeEqualUsingFiel...44887a5fix(kotest-framework-engine): Use display-name rendering for ignored tests in...b68ef3edisable return-value-checker for releases (#6165)86a7d93Fix typo in proptest config docs (#6155)1677bd5Add documentation for OffsetDateTime tolerance matcher (#6161)47f3efcDocument instant-based ZonedDateTimeplusOrMinuscomparison (#6159)- See full diff in compare view
Updates `io.kotest:kotest-assertions-json` from 6.2.1 to 6.2.2Release notes
Sourced from io.kotest:kotest-assertions-json's releases.
v6.2.2
What's Changed
- Document instant-based ZonedDateTime
plusOrMinuscomparison by@AlexCue987with@Copilotin kotest/kotest#6159- Add documentation for OffsetDateTime tolerance matcher by
@AlexCue987in kotest/kotest#6161- Fix typo in proptest config docs by
@axiomin kotest/kotest#6155- disable return-value-checker for releases by
@alfonsoristoratoin kotest/kotest#6165- fix(kotest-framework-engine): Use display-name rendering for ignored tests in TeamCity listener by
@sksamuelin kotest/kotest#6119- fix(kotest-assertions-core): Handle null map values in shouldBeEqualUsingFields by
@sksamuelin kotest/kotest#6112- Update dependency io.mockk:mockk to v1.14.11 by
@renovate[bot] in kotest/kotest#6163- fix: attach Fail fast scope tracker for each root test case in instance per spec mode by
@ashdawngaryin kotest/kotest#6160- Update dependency io.projectreactor.tools:blockhound to v1.0.17.RELEASE by
@renovate[bot] in kotest/kotest#6164New Contributors
@AlexCue987with@Copilotmade their first contribution in kotest/kotest#6159@axiommade their first contribution in kotest/kotest#6155@ashdawngarymade their first contribution in kotest/kotest#6160Full Changelog: https://github.com/kotest/kotest/compare/v6.2.1...v6.2.2
Commits
4e55e4cUpdate dependency io.projectreactor.tools:blockhound to v1.0.17.RELEASE (#6164)3e0202ffix: attach Fail fast scope tracker for each root test case in instance per s...134c12bUpdate dependency io.mockk:mockk to v1.14.11 (#6163)01c7429fix(kotest-assertions-core): Handle null map values in shouldBeEqualUsingFiel...44887a5fix(kotest-framework-engine): Use display-name rendering for ignored tests in...b68ef3edisable return-value-checker for releases (#6165)86a7d93Fix typo in proptest config docs (#6155)1677bd5Add documentation for OffsetDateTime tolerance matcher (#6161)47f3efcDocument instant-based ZonedDateTimeplusOrMinuscomparison (#6159)- See full diff in compare view
Updates `io.kotest:kotest-assertions-ktor` from 6.2.1 to 6.2.2Release notes
Sourced from io.kotest:kotest-assertions-ktor's releases.
v6.2.2
What's Changed
- Document instant-based ZonedDateTime
plusOrMinuscomparison by@AlexCue987with@Copilotin kotest/kotest#6159- Add documentation for OffsetDateTime tolerance matcher by
@AlexCue987in kotest/kotest#6161- Fix typo in proptest config docs by
@axiomin kotest/kotest#6155- disable return-value-checker for releases by
@alfonsoristoratoin kotest/kotest#6165- fix(kotest-framework-engine): Use display-name rendering for ignored tests in TeamCity listener by
@sksamuelin kotest/kotest#6119- fix(kotest-assertions-core): Handle null map values in shouldBeEqualUsingFields by
@sksamuelin kotest/kotest#6112- Update dependency io.mockk:mockk to v1.14.11 by
@renovate[bot] in kotest/kotest#6163- fix: attach Fail fast scope tracker for each root test case in instance per spec mode by
@ashdawngaryin kotest/kotest#6160- Update dependency io.projectreactor.tools:blockhound to v1.0.17.RELEASE by
@renovate[bot] in kotest/kotest#6164New Contributors
@AlexCue987with@Copilotmade their first contribution in kotest/kotest#6159@axiommade their first contribution in kotest/kotest#6155@ashdawngarymade their first contribution in kotest/kotest#6160Full Changelog: https://github.com/kotest/kotest/compare/v6.2.1...v6.2.2
Commits
4e55e4cUpdate dependency io.projectreactor.tools:blockhound to v1.0.17.RELEASE (#6164)3e0202ffix: attach Fail fast scope tracker for each root test case in instance per s...134c12bUpdate dependency io.mockk:mockk to v1.14.11 (#6163)01c7429fix(kotest-assertions-core): Handle null map values in shouldBeEqualUsingFiel...44887a5fix(kotest-framework-engine): Use display-name rendering for ignored tests in...b68ef3edisable return-value-checker for releases (#6165)86a7d93Fix typo in proptest config docs (#6155)1677bd5Add documentation for OffsetDateTime tolerance matcher (#6161)47f3efcDocument instant-based ZonedDateTimeplusOrMinuscomparison (#6159)- See full diff in compare view
Updates `io.kotest:kotest-assertions-json` from 6.2.1 to 6.2.2Release notes
Sourced from io.kotest:kotest-assertions-json's releases.
v6.2.2
What's Changed
- Document instant-based ZonedDateTime
plusOrMinuscomparison by@AlexCue987with@Copilotin kotest/kotest#6159- Add documentation for OffsetDateTime tolerance matcher by
@AlexCue987in kotest/kotest#6161- Fix typo in proptest config docs by
@axiomin kotest/kotest#6155- disable return-value-checker for releases by
@alfonsoristoratoin kotest/kotest#6165- fix(kotest-framework-engine): Use display-name rendering for ignored tests in TeamCity listener by
@sksamuelin kotest/kotest#6119- fix(kotest-assertions-core): Handle null map values in shouldBeEqualUsingFields by
@sksamuelin kotest/kotest#6112- Update dependency io.mockk:mockk to v1.14.11 by
@renovate[bot] in kotest/kotest#6163- fix: attach Fail fast scope tracker for each root test case in instance per spec mode by
@ashdawngaryin kotest/kotest#6160- Update dependency io.projectreactor.tools:blockhound to v1.0.17.RELEASE by
@renovate[bot] in kotest/kotest#6164New Contributors
@AlexCue987with@Copilotmade their first contribution in kotest/kotest#6159@axiommade their first contribution in kotest/kotest#6155@ashdawngarymade their first contribution in kotest/kotest#6160Full Changelog: https://github.com/kotest/kotest/compare/v6.2.1...v6.2.2
Commits
4e55e4cUpdate dependency io.projectreactor.tools:blockhound to v1.0.17.RELEASE (#6164)3e0202ffix: attach Fail fast scope tracker for each root test case in instance per s...134c12bUpdate dependency io.mockk:mockk to v1.14.11 (#6163)01c7429fix(kotest-assertions-core): Handle null map values in shouldBeEqualUsingFiel...44887a5fix(kotest-framework-engine): Use display-name rendering for ignored tests in...b68ef3edisable return-value-checker for releases (#6165)86a7d93Fix typo in proptest config docs (#6155)1677bd5Add documentation for OffsetDateTime tolerance matcher (#6161)47f3efcDocument instant-based ZonedDateTimeplusOrMinuscomparison (#6159)- See full diff in compare view
Updates `io.kotest:kotest-assertions-ktor` from 6.2.1 to 6.2.2Release notes
Sourced from io.kotest:kotest-assertions-ktor's releases.
v6.2.2
What's Changed
- Document instant-based ZonedDateTime
plusOrMinuscomparison by@AlexCue987with@Copilotin kotest/kotest#6159- Add documentation for OffsetDateTime tolerance matcher by
@AlexCue987in kotest/kotest#6161- Fix typo in proptest config docs by
@axiomin kotest/kotest#6155- disable return-value-checker for releases by
@alfonsoristoratoin kotest/kotest#6165- fix(kotest-framework-engine): Use display-name rendering for ignored tests in TeamCity listener by
@sksamuelin kotest/kotest#6119- fix(kotest-assertions-core): Handle null map values in shouldBeEqualUsingFields by
@sksamuelin kotest/kotest#6112- Update dependency io.mockk:mockk to v1.14.11 by
@renovate[bot] in kotest/kotest#6163- fix: attach Fail fast scope tracker for each root test case in instance per spec mode by
@ashdawngaryin kotest/kotest#6160- Update dependency io.projectreactor.tools:blockhound to v1.0.17.RELEASE by
@renovate[bot] in kotest/kotest#6164New Contributors
@AlexCue987with@Copilotmade their first contribution in kotest/kotest#6159@axiommade their first contribution in kotest/kotest#6155@ashdawngarymade their first contribution in kotest/kotest#6160Full Changelog: https://github.com/kotest/kotest/compare/v6.2.1...v6.2.2
Commits
4e55e4cUpdate dependency io.projectreactor.tools:blockhound to v1.0.17.RELEASE (#6164)3e0202ffix: attach Fail fast scope tracker for each root test case in instance per s...134c12bUpdate dependency io.mockk:mockk to v1.14.11 (#6163)01c7429fix(kotest-assertions-core): Handle null map values in shouldBeEqualUsingFiel...44887a5fix(kotest-framework-engine): Use display-name rendering for ignored tests in...b68ef3edisable return-value-checker for releases (#6165)86a7d93Fix typo in proptest config docs (#6155)1677bd5Add documentation for OffsetDateTime tolerance matcher (#6161)47f3efcDocument instant-based ZonedDateTimeplusOrMinuscomparison (#6159)- See full diff in compare view
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d9b5de..035058d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ netty = "4.2.15.Final" awaitility = "4.3.0" collections-immutable = "0.5.1" coroutines = "1.11.0" -kotest = "6.2.1" +kotest = "6.2.2" kotlinx-io = "0.9.1" ktor = "3.5.1" logging = "8.0.4" From dbd34ff327c654e3811326192cc0bceb2543992e Mon Sep 17 00:00:00 2001 From: Klyne Chrysler Date: Thu, 16 Jul 2026 00:38:21 +0800 Subject: [PATCH 026/106] close stateless streamable http session after request completion (#872) fixes #786 stateless streamable http endpoint never closed its per request session, so the session registry and notification subscriptions grew forever. now the session is closed in a finally block after each request, which triggers the existing cleanup. also removed the per request server.onClose logging callback that accumulated on shared servers. added tests proving sessions are removed after normal and rejected requests. jvmTest, ktlint, detekt, apiCheck all pass. no public api changes. Co-authored-by: Pavel Gorgulov --- .../kotlin/sdk/server/KtorServer.kt | 14 ++- ...elessStreamableHttpSessionLifecycleTest.kt | 100 ++++++++++++++++++ 2 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StatelessStreamableHttpSessionLifecycleTest.kt diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt index ec0652b..73ef258 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt @@ -380,11 +380,17 @@ private suspend fun RoutingContext.mcpStatelessStreamableHttpEndpoint( logger.info { "New stateless StreamableHttp connection established without sessionId" } val server = block() - server.onClose { logger.info { "Server connection closed without sessionId" } } - server.createSession(transport) + val session = server.createSession(transport) - transport.handleRequest(null, this.call) - logger.debug { "Server connected to transport without sessionId" } + try { + transport.handleRequest(null, this.call) + logger.debug { "Server connected to transport without sessionId" } + } finally { + // A stateless session serves exactly one request: close it so the server's + // session registry and notification subscriptions do not grow unboundedly. + session.close() + logger.debug { "Stateless session closed after request completion" } + } } private suspend fun RoutingContext.mcpPostEndpoint(transportManager: TransportManager ) { diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StatelessStreamableHttpSessionLifecycleTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StatelessStreamableHttpSessionLifecycleTest.kt new file mode 100644 index 0000000..3646faf --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StatelessStreamableHttpSessionLifecycleTest.kt @@ -0,0 +1,100 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.matchers.maps.shouldBeEmpty +import io.kotest.matchers.shouldBe +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.server.testing.testApplication +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +/** + * Verifies that the stateless Streamable HTTP endpoint does not leak server sessions: + * every session created to serve a single request must be closed and removed from the + * session registry once that request completes (https://github.com/modelcontextprotocol/kotlin-sdk/issues/786). + */ +class StatelessStreamableHttpSessionLifecycleTest { + + private fun testServer(): Server = Server( + Implementation("test-server", "1.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + + @Test + fun `stateless endpoint removes session after each request`() = testApplication { + val server = testServer() + application { + mcpStatelessStreamableHttp { server } + } + + repeat(5) { + val response = client.post("/mcp") { + addStreamableHeaders() + setBody(initializeRequestBody()) + } + response.status shouldBe HttpStatusCode.OK + } + + eventually(5.seconds) { + server.sessions.shouldBeEmpty() + } + } + + @Test + fun `stateless endpoint removes session when the request is rejected`() = testApplication { + val server = testServer() + application { + mcpStatelessStreamableHttp { server } + } + + // Missing "text/event-stream" in Accept makes the transport reject the request. + val response = client.post("/mcp") { + header(HttpHeaders.Host, "localhost") + header(HttpHeaders.Accept, ContentType.Application.Json.toString()) + contentType(ContentType.Application.Json) + setBody(initializeRequestBody()) + } + response.status shouldBe HttpStatusCode.NotAcceptable + + eventually(5.seconds) { + server.sessions.shouldBeEmpty() + } + } + + private fun HttpRequestBuilder.addStreamableHeaders() { + header(HttpHeaders.Host, "localhost") + header( + HttpHeaders.Accept, + listOf(ContentType.Application.Json, ContentType.Text.EventStream).joinToString(", "), + ) + contentType(ContentType.Application.Json) + } + + private fun initializeRequestBody(): String { + val request = InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = "test-client", version = "1.0.0"), + ), + ).toJSON() + + return McpJson.encodeToString(JSONRPCMessage.serializer(), request) + } +} From e464e69a081aea36cb5d11393e6f2ae4d4eb9c9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:43:10 +0200 Subject: [PATCH 027/106] chore(deps): bump io.netty:netty-bom from 4.2.15.Final to 4.2.16.Final in the infrastructure group across 1 directory (#889) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the infrastructure group with 1 update in the / directory: [io.netty:netty-bom](https://github.com/netty/netty). Updates `io.netty:netty-bom` from 4.2.15.Final to 4.2.16.Final Release notes
Sourced from io.netty:netty-bom's releases.
netty-4.2.16.Final
What's Changed
- Document Java 9 requirement for io_uring by
@jchambersin netty/netty#16904- Add BlockHound exception for DnsQueryIdSpace by
@violetaggin netty/netty#16896- Fix incorrect bounds in error message of HpackDecoder.setMaxHeaderListSize by
@skyguard1in netty/netty#16901- Add epoch-based chunk cache purge with ring buffer for thread-local reuse by
@franz1981in netty/netty#16766- Use Splittable/ThreadLocalRandom to generate bulk data in tests by
@chrisvestin netty/netty#16808- Auto-port 4.2: SingleThreadEventExecutor: document Throwable safety contract on run() by
@netty-project-botin netty/netty#16909- Auto-port 4.2: Make HTTP/2 frame hashCode consistent with equals by
@netty-project-botin netty/netty#16910- Auto-port 4.2: MQTT: Make the decodeProperties early-REPLAY check actually fire by
@netty-project-botin netty/netty#16919- IoUring: fix io_uring datagram writes with non-zero readerIndex by
@dreamlike-oceanin netty/netty#16905- Exclude internal events from IoHandler.run() return value in epoll, io_uring and kqueue by
@franz1981in netty/netty#16848- IoUring: Pass IORING_ENTER_NO_IOWAIT to report accurate CPU usage by
@winewayin netty/netty#16739- Avoid logging exceptions that tests ignore by
@chrisvestin netty/netty#16891- Reject control characters at the boundary of HTTP method names by
@daguimuin netty/netty#16723- IoUring: fix TCP Fast Open initial writes with readerIndex and composites by
@dreamlike-oceanin netty/netty#16929- Try to fix/stabilize a number of flaky tests by
@chrisvestin netty/netty#16934- Fix propagation of startTls for client SslContext handlers by
@skyguard1in netty/netty#16931- Update to latest tcnative release by
@normanmaurerin netty/netty#16936- Move test to shared testsuite by
@normanmaurerin netty/netty#16928- [Refactor] Useful helper method getOrDefault & cleaner abstraction by
@sanjomoin netty/netty#16927- Make permessage-deflate server window size and memLevel configurable by
@fru1tworldin netty/netty#16809- Return early in DnsQueryContext.writeQuery when the query ID space is exhausted by
@HwangRockin netty/netty#16950- Fix HTTP 2 PUSH_PROMISE stream association validation by
@skyguard1in netty/netty#16952- Fix GZIP FEXTRA extra-field handling in JdkZlibDecoder by
@daguimuin netty/netty#16951- Http3FrameCodec handle fragmented payloads when skipping unknown frames by
@skyguard1in netty/netty#16960- Add opt-in validation of mandatory pseudo-header fields for HTTP/2 by
@hyperxproin netty/netty#16932- Strictly validate MQTT UTF-8 Encoded String by
@skyguard1in netty/netty#16939- Stop DateFormatter trailing token from running past the parse end by
@daguimuin netty/netty#16958- IpFilter: Deprecate constructor which use accept by default by
@normanmaurerin netty/netty#16961- Add RFC 10008 QUERY Method support by
@desiderantesin netty/netty#16966- Correctly release and fail queued traffic-shaping writes on close by
@skyguard1in netty/netty#16959- Reject control characters at the boundary of the HTTP version token by
@HwangRockin netty/netty#16971- FlowControlHandler: respect auto-read when toggled while dequeueing by
@schiemonin netty/netty#16949- Fix leak in ReferenceCountedOpenSslEngine.addCredential by
@jmcrawford45in netty/netty#16979- IdleStateHandler: reset firstWriter/ReaderIdleEvent in resetWriteTimeout/resetReadTimeout by
@husseinvr97in netty/netty#16982- Fix typo in AbstractSniHandler Javadoc by
@coderbruisin netty/netty#16988- Fix client/server inconsistency in SslCredential support matrix by
@jmcrawford45in netty/netty#16990- Reconcile
AbstractCoalescingBufferQueuereadableBytes when it drains, and fail stuck HTTP/2 streams instead of spinning empty DATA frames by@gavinbunneyin netty/netty#16947- Use Ticker in Http2MaxRstFrameListener for testability by
@skyguard1in netty/netty#16993- Reset UTF-8 decode state on CR in StompSubframeDecoder by
@vasiliy-mikhailovin netty/netty#16991- FastLz: Guard decompression against truncated input by
@yawkatin netty/netty#17000- Reject non-token characters in HTTP/2 header names by
@daguimuin netty/netty#16762- Auto-port 4.2: Fix SelfSignCertificate initialization in tests by
@netty-project-botin netty/netty#17029- Enable extension of Http3ClientConnectionHandler to support higher-level protocols such as WebTransport. by
@sanjomoin netty/netty#17027- Implement Adaptive Cumulator by
@shivaspeaksin netty/netty#16731- Allow WebSocket extension negotiation to be disabled per response by
@mkurzin netty/netty#17030- Support QPACK sensitivity detector for Never Indexed header fields by
@skyguard1in netty/netty#17026- Fix maxAllocation for brotli-encoded content in HttpContentDecompressor by
@skyguard1in netty/netty#17037- Pin github actions to reduce risk by
@normanmaurerin netty/netty#17043... (truncated)
Commits
3703d79[maven-release-plugin] prepare release netty-4.2.16.Final63bbb2cUpdate rust toolchain - add required parametersac06c1bUpdate rust toolchain5b68c61Merge branches from forks (#17063)de5d276Update lz4-java to 1.11.1 (#17061)da22048Pin github actions to reduce risk (#17043)0332676Fix maxAllocation for brotli-encoded content in HttpContentDecompressor (#17037)7364401Support QPACK sensitivity detector for Never Indexed header fields (#17026)06faf18Allow WebSocket extension negotiation to be disabled per response (#17030)bc4b983Implement Adaptive Cumulator (#16731)- Additional commits viewable in compare view
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 035058d..235f0e9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ kover = "0.9.8" ktlint = "14.2.0" knit = "0.5.1" mavenPublish = "0.37.0" -netty = "4.2.15.Final" +netty = "4.2.16.Final" # libraries version awaitility = "4.3.0" From c984bc7a327dd251d42cea0b7cdbb1ecf7ffb561 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:39:09 +0200 Subject: [PATCH 028/106] chore(deps): bump ch.qos.logback:logback-classic from 1.5.37 to 1.5.38 (#890) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.37 to 1.5.38. Release notes
Sourced from ch.qos.logback:logback-classic's releases.
Logback 1.5.38
2026-07-09 Release of logback version 1.5.38
• In
HardenedObjectInputStream, fixed a typo preventingThrowableobjects from being white-filtered. This issue was reported in [PR #1045](qos-ch/logback#1045) by t0rchwo0d.• A bitwise identical binary of this version can be reproduced by building from source code at commit d04984a41fce42977466f45a2f076f0ee5cc4207 associated with the tag v_1.5.38. Release built using Java "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian 11.6.
Commits
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 235f0e9..8591f0d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ logging = "8.0.4" mockk = "1.14.11" serialization = "1.11.0" slf4j = "2.0.18" -logback = "1.5.37" +logback = "1.5.38" junit = "5.14.4" [libraries] From 5cb76fd0401dc33ff279520456dfee62033f363e Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Thu, 16 Jul 2026 15:41:46 +0200 Subject: [PATCH 029/106] fix(core): clear recently-cancelled request ids on connection close (#892) Clear the recently-cancelled request id buffer in `doClose` so connection teardown fully resets per-request bookkeeping. ## Motivation and Context Follow-up to #884. `doClose` already resets `_responseHandlers`, `_progressHandlers`, and `inFlightRequestJobs`, but left `recentlyCancelledRequestIds` populated across a close. This aligns teardown to clear all Protocol-scoped per-request state. Not a correctness bug: the buffer is bounded (256) and self-evicting, request ids are random UUIDs so stale entries cannot false-match a new connection, and the post-close `onMessage` guard drops late messages from the old transport before the buffer is consulted. The change removes the lone asymmetry in `doClose`. ## How Has This Been Tested? - `./gradlew :kotlin-sdk-core:compileKotlinJvm` - `./gradlew :kotlin-sdk-core:jvmTest --tests "*ProtocolTest*"` ## Breaking Changes None. ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [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 - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context `recentlyCancelledRequestIds` is Protocol-scoped (like `_responseHandlers`/`_progressHandlers`), so it is reset in `doClose` rather than on the per-connection `Connection` object. --- .../kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 3515b4a..0c82f5e 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -436,6 +436,7 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio } val handlersToNotify = _responseHandlers.getAndSet(persistentMapOf()).values.toList() _progressHandlers.getAndSet(persistentMapOf()) + recentlyCancelledRequestIds.getAndSet(persistentListOf()) connection.inFlightRequestJobs.getAndSet(persistentMapOf()) connection.handlerScope.cancel() onClose() From 309d88fe1d4c3e2553553031290b001d65587fef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:58:22 +0200 Subject: [PATCH 030/106] chore(deps): bump the other-dependencies group across 2 directories with 3 updates (#897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the other-dependencies group with 2 updates in the /samples/kotlinlang-mcp-server directory: [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) and [com.gradleup.shadow](https://github.com/GradleUp/shadow). Bumps the other-dependencies group with 2 updates in the /samples/simple-streamable-server directory: [com.gradleup.shadow](https://github.com/GradleUp/shadow) and [org.junit.jupiter:junit-jupiter](https://github.com/junit-team/junit-framework). Updates `ch.qos.logback:logback-classic` from 1.5.37 to 1.5.38 Release notes
Sourced from ch.qos.logback:logback-classic's releases.
Logback 1.5.38
2026-07-09 Release of logback version 1.5.38
• In
HardenedObjectInputStream, fixed a typo preventingThrowableobjects from being white-filtered. This issue was reported in [PR #1045](qos-ch/logback#1045) by t0rchwo0d.• A bitwise identical binary of this version can be reproduced by building from source code at commit d04984a41fce42977466f45a2f076f0ee5cc4207 associated with the tag v_1.5.38. Release built using Java "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian 11.6.
Commits
Updates `com.gradleup.shadow` from 9.5.1 to 9.6.0Release notes
Sourced from com.gradleup.shadow's releases.
9.6.0
Added
- Extract R8 rules from dependency JARs when using
minimize { r8 { ... } }. (#2089)Changed
- Rename
ShadowDslMarkertoShadowDsl. (#2091)- POTENTIALLY BREAKING: Apply
@ShadowDsltoShadowJar,ResourceTransformer,DependencyFilter, andRelocator. (#2090)
This restricts nested DSL configuration blocks from implicitly calling outer receiver APIs in Kotlin script files.Fixed
- Validate ZIP entry names to prevent Zip Slip path traversal.
- Avoid resolving unused R8 dependency. (#2101)
Commits
fb36668Prepare version 9.6.09e5c06dTest relocation of ModuleMainClass in module-info (#2109)e37770cTest relocation of nested class signatures (#2108)74792f7Add unit tests for internal components (#2107)b590819Avoid resolving unused R8 dependency (#2101)1dd8db4Update plugin android-lint to v9.3.0 (#2104)083a01aDefer shadowR8 configuration (#2100)1ce486dUpdate kotlin monorepo to v2.4.10 (#2103)c89473eTest allowing custom ShadowJar without shadowR8 registered (#2102)f23f5c9Update dependency org.junit:junit-bom to v6.1.2 (#2098)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.5.1 to 9.6.0Release notes
Sourced from com.gradleup.shadow's releases.
9.6.0
Added
- Extract R8 rules from dependency JARs when using
minimize { r8 { ... } }. (#2089)Changed
- Rename
ShadowDslMarkertoShadowDsl. (#2091)- POTENTIALLY BREAKING: Apply
@ShadowDsltoShadowJar,ResourceTransformer,DependencyFilter, andRelocator. (#2090)
This restricts nested DSL configuration blocks from implicitly calling outer receiver APIs in Kotlin script files.Fixed
- Validate ZIP entry names to prevent Zip Slip path traversal.
- Avoid resolving unused R8 dependency. (#2101)
Commits
fb36668Prepare version 9.6.09e5c06dTest relocation of ModuleMainClass in module-info (#2109)e37770cTest relocation of nested class signatures (#2108)74792f7Add unit tests for internal components (#2107)b590819Avoid resolving unused R8 dependency (#2101)1dd8db4Update plugin android-lint to v9.3.0 (#2104)083a01aDefer shadowR8 configuration (#2100)1ce486dUpdate kotlin monorepo to v2.4.10 (#2103)c89473eTest allowing custom ShadowJar without shadowR8 registered (#2102)f23f5c9Update dependency org.junit:junit-bom to v6.1.2 (#2098)- Additional commits viewable in compare view
Updates `com.gradleup.shadow` from 9.5.1 to 9.6.0Release notes
Sourced from com.gradleup.shadow's releases.
9.6.0
Added
- Extract R8 rules from dependency JARs when using
minimize { r8 { ... } }. (#2089)Changed
- Rename
ShadowDslMarkertoShadowDsl. (#2091)- POTENTIALLY BREAKING: Apply
@ShadowDsltoShadowJar,ResourceTransformer,DependencyFilter, andRelocator. (#2090)
This restricts nested DSL configuration blocks from implicitly calling outer receiver APIs in Kotlin script files.Fixed
- Validate ZIP entry names to prevent Zip Slip path traversal.
- Avoid resolving unused R8 dependency. (#2101)
Commits
fb36668Prepare version 9.6.09e5c06dTest relocation of ModuleMainClass in module-info (#2109)e37770cTest relocation of nested class signatures (#2108)74792f7Add unit tests for internal components (#2107)b590819Avoid resolving unused R8 dependency (#2101)1dd8db4Update plugin android-lint to v9.3.0 (#2104)083a01aDefer shadowR8 configuration (#2100)1ce486dUpdate kotlin monorepo to v2.4.10 (#2103)c89473eTest allowing custom ShadowJar without shadowR8 registered (#2102)f23f5c9Update dependency org.junit:junit-bom to v6.1.2 (#2098)- Additional commits viewable in compare view
Updates `org.junit.jupiter:junit-jupiter` from 6.1.1 to 6.1.2Release notes
Sourced from org.junit.jupiter:junit-jupiter's releases.
JUnit 6.1.2 = Platform 6.1.2 + Jupiter 6.1.2 + Vintage 6.1.2
See Release Notes.
Full Changelog: https://github.com/junit-team/junit-framework/compare/r6.1.1...r6.1.2
Commits
b685426Release 6.1.2ae244a6Remove blanket outputDirectoryCreator from SuiteEngineTests (#5793)43bd154Finalize 6.1.2 release notes0cd9510Fix order of release note sectionsa1507cbAdd initial 6.1.2 release notes to release notes index9326641Fix NoTestsDiscoveredException for suites containing only dynamic tests (#5839)2ef1123Create initial 6.1.2 release notes from template83fa9abBack to snapshots for further development- See full diff in compare view
Updates `com.gradleup.shadow` from 9.5.1 to 9.6.0Release notes
Sourced from com.gradleup.shadow's releases.
9.6.0
Added
- Extract R8 rules from dependency JARs when using
minimize { r8 { ... } }. (#2089)Changed
- Rename
ShadowDslMarkertoShadowDsl. (#2091)- POTENTIALLY BREAKING: Apply
@ShadowDsltoShadowJar,ResourceTransformer,DependencyFilter, andRelocator. (#2090)
This restricts nested DSL configuration blocks from implicitly calling outer receiver APIs in Kotlin script files.Fixed
- Validate ZIP entry names to prevent Zip Slip path traversal.
- Avoid resolving unused R8 dependency. (#2101)
Commits
fb36668Prepare version 9.6.09e5c06dTest relocation of ModuleMainClass in module-info (#2109)e37770cTest relocation of nested class signatures (#2108)74792f7Add unit tests for internal components (#2107)b590819Avoid resolving unused R8 dependency (#2101)1dd8db4Update plugin android-lint to v9.3.0 (#2104)083a01aDefer shadowR8 configuration (#2100)1ce486dUpdate kotlin monorepo to v2.4.10 (#2103)c89473eTest allowing custom ShadowJar without shadowR8 registered (#2102)f23f5c9Update dependency org.junit:junit-bom to v6.1.2 (#2098)- Additional commits viewable in compare view
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---Signed-off-by: dependabot[bot]Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot showignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlinlang-mcp-server/gradle/libs.versions.toml | 4 ++-- samples/simple-streamable-server/gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml index 029aae2..047c0bf 100644 --- a/samples/kotlinlang-mcp-server/gradle/libs.versions.toml +++ b/samples/kotlinlang-mcp-server/gradle/libs.versions.toml @@ -4,8 +4,8 @@ ktor = "3.5.1" mcp-kotlin = "0.14.0" serialization = "1.11.0" coroutines = "1.11.0" -logback = "1.5.37" -shadow = "9.5.1" +logback = "1.5.38" +shadow = "9.6.0" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } diff --git a/samples/simple-streamable-server/gradle/libs.versions.toml b/samples/simple-streamable-server/gradle/libs.versions.toml index 376bc9b..bbf7120 100644 --- a/samples/simple-streamable-server/gradle/libs.versions.toml +++ b/samples/simple-streamable-server/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] kotlin = "2.4.0" ktor = "3.5.1" -junit = "6.1.1" +junit = "6.1.2" mcp-kotlin = "0.14.0" slf4j = "2.0.18" -shadow = "9.5.1" +shadow = "9.6.0" [libraries] ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } From 0ea42519ed6e83ef3d8e3ef928e1f0df6e96115c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:46:44 +0200 Subject: [PATCH 031/106] chore(deps): bump org.jetbrains.kotlin.jvm from 2.4.0 to 2.4.10 in /samples/kotlin-mcp-client (#899) Bumps [org.jetbrains.kotlin.jvm](https://github.com/JetBrains/kotlin) from 2.4.0 to 2.4.10. Release notes
Sourced from org.jetbrains.kotlin.jvm's releases.
Kotlin 2.4.10
Changelog
Backend. Wasm
KT-87066K/Wasm: Not all files are presented in compiler output directory with multimodule-closed-world and incremental compilationCompiler
KT-86939JVM: IllegalStateException "No value for annotation parameter" when using const val in nested Java annotation array argumentKT-83766K2: Wrong sourcePsi is set forSymbolPsiLiteralin SLC for annotation arguments referencing a const valKT-86728Reified type inference: expected type not propagated into inline call inside lambda with elvis operatorCompose Compiler
b/522127447Compose Compiler 2.4: classes previously inferredstablenow reportedruntime/UncertainKlibs
KT-86501Native: IrTypeAliasSymbolImpl is already bound. Signature: kotlinx.datetime/Instant|null[0] on iosSimulatorArm64Tools. CLI
KT-86930Introducekotlinrin the Kotlin distributionTools. Gradle. BCV
KT-87223Gradle, BCV: open version range in kotlinAbiValidationCompatClasspath causes kotlin-build-tools-impl to resolve to 2.4.20-Beta1 instead of 2.4.0Tools. Gradle. JS
KT-87304jsBrowserTest fails with "exited with errors (exit code: 1)"KT-86057kotlinUpgradeYarnLock skips lock file regeneration when kotlinNpmInstall is up-to-date, causing kotlinStoreYarnLock to failTools. Gradle. Multiplatform
KT-87084False positive warning for JS and Wasm compilations when CRI is enabledTools. Scripts
KT-87076@file:CompilerOptions("-jvm-target", ...) ignored in .main.kts scripts in Kotlin 2.4.0, falling back to JVM target 1.8KT-86352K2 scripting: FirResolvedTypeRef exception when resolving extension functions from imported scriptsKotlin 2.4.10-RC2
Changelog
Backend. Wasm
KT-87066K/Wasm: Not all files are presented in compiler output directory with multimodule-closed-world and incremental compilation... (truncated)
Changelog
Sourced from org.jetbrains.kotlin.jvm's changelog.
2.4.10
Backend. Wasm
KT-87066K/Wasm: Not all files are presented in compiler output directory with multimodule-closed-world and incremental compilationCompiler
KT-86939JVM: IllegalStateException "No value for annotation parameter" when using const val in nested Java annotation array argumentKT-83766K2: Wrong sourcePsi is set forSymbolPsiLiteralin SLC for annotation arguments referencing a const valKT-86728Reified type inference: expected type not propagated into inline call inside lambda with elvis operatorCompose Compiler
b/522127447Compose Compiler 2.4: classes previously inferredstablenow reportedruntime/UncertainKlibs
KT-86501Native: IrTypeAliasSymbolImpl is already bound. Signature: kotlinx.datetime/Instant|null[0] on iosSimulatorArm64Tools. CLI
KT-86930Introducekotlinrin the Kotlin distributionTools. Gradle. BCV
KT-87223Gradle, BCV: open version range in kotlinAbiValidationCompatClasspath causes kotlin-build-tools-impl to resolve to 2.4.20-Beta1 instead of 2.4.0Tools. Gradle. JS
KT-87304jsBrowserTest fails with "exited with errors (exit code: 1)"KT-86057kotlinUpgradeYarnLock skips lock file regeneration when kotlinNpmInstall is up-to-date, causing kotlinStoreYarnLock to failTools. Gradle. Multiplatform
KT-87084False positive warning for JS and Wasm compilations when CRI is enabledTools. Scripts
Commits
5687445Add Changelog for 2.4.10-RC2ba30392[Wasm] Support regenerate unchanged modules flag on KGP6307e73[Wasm] Implement Xwasm-IC-generate-unchanged-modules flag39d7aeeAdd ChangeLog for 2.4.10-RC2a2956be[Gradle] Update karma.conf.js to use 'require(...)' instead plain strings9b1361c[Gradle] KT-87223: Updated regression testa4c153a[Gradle] KT-87223: Fix toochain to 2.4.0 for abiValidation compat configbc04b90Add ChangeLog for 2.4.10-RCbbcb94b[CRI] explicitly disable CRI generation for non-JVM targets to avoid false-po...6c2c458Scripting: fix jvmTarget processing from script annotations- Additional commits viewable in compare view
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- samples/kotlin-mcp-client/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/kotlin-mcp-client/gradle/libs.versions.toml b/samples/kotlin-mcp-client/gradle/libs.versions.toml index f674a64..732dfbd 100644 --- a/samples/kotlin-mcp-client/gradle/libs.versions.toml +++ b/samples/kotlin-mcp-client/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] anthropic = "2.48.0" -kotlin = "2.4.0" +kotlin = "2.4.10" ktor = "3.5.1" mcp-kotlin = "0.14.0" shadow = "9.5.1" From 078e739424af944089cfab48a0a0ed271f161950 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:49:15 +0200 Subject: [PATCH 032/106] chore(deps): bump org.jetbrains.kotlinx.kover from 0.9.8 to 0.9.9 in the gradle-plugins group across 1 directory (#902) Bumps the gradle-plugins group with 1 update in the / directory: [org.jetbrains.kotlinx.kover](https://github.com/Kotlin/kotlinx-kover). Updates `org.jetbrains.kotlinx.kover` from 0.9.8 to 0.9.9 Release notes
Sourced from org.jetbrains.kotlinx.kover's releases.
0.9.9
Kover Gradle Plugin
Bugfixes
[#818](https://github.com/Kotlin/kotlinx-kover/issues/818)Fixed Gradle 9.6.0 deprecation warning[#810](https://github.com/Kotlin/kotlinx-kover/issues/810)Fix 0% coverage for classes in com.android.* packagesChangelog
Sourced from org.jetbrains.kotlinx.kover's changelog.
0.9.9 / 2026-07-17
Kover Gradle Plugin
Bugfixes
[#818](https://github.com/Kotlin/kotlinx-kover/issues/818)Fixed Gradle 9.6.0 deprecation warning[#810](https://github.com/Kotlin/kotlinx-kover/issues/810)Fix 0% coverage for classes in com.android.* packagesCommits
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8591f0d..6d3c5a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ binaryCompatibilityValidatorPlugin = "0.18.1" detekt = "2.0.0-alpha.5" dokka = "2.2.0" kotlin = "2.4.0" -kover = "0.9.8" +kover = "0.9.9" ktlint = "14.2.0" knit = "0.5.1" mavenPublish = "0.37.0" From 737b46f03299f038270b30176fc3b2f849770266 Mon Sep 17 00:00:00 2001 From: Takuya Akamatsu Date: Wed, 22 Jul 2026 01:17:16 +0900 Subject: [PATCH 033/106] feat(server): validate accepted elicitation content against the requested schema (#896) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validate accepted form-mode elicitation `content` against the `requestedSchema` on the server, rejecting mismatches with an `INVALID_PARAMS` error. fixes #895 ## Motivation and Context `createElicitation` returned accepted content to server code as-is. The specification's form-mode security guidance says "Servers SHOULD validate received data matches the requested schema" ([2025-11-25, Form Mode Security](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#form-mode-security)). Comparable validation already exists in the TypeScript SDK's `elicitInput` and Python's `elicit_with_validation` helper. Details and design notes are in #895. ## How Has This Been Tested? New unit tests for the validator and integration tests over an in-memory client–server pair. ## Breaking Changes No API changes. Behavioral note: an accepted form-mode response that does not match the requested schema now fails `createElicitation` with `McpException` (`INVALID_PARAMS`) instead of being returned to the caller. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed Co-authored-by: Pavel Gorgulov --- .../kotlin/sdk/client/ClientTest.kt | 97 ++++ .../kotlin/sdk/server/ClientConnection.kt | 22 +- .../sdk/server/ElicitationValidation.kt | 174 +++++++ .../kotlin/sdk/server/ServerSession.kt | 7 +- .../sdk/server/ElicitationValidationTest.kt | 438 ++++++++++++++++++ 5 files changed, 735 insertions(+), 3 deletions(-) create mode 100644 kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidation.kt create mode 100644 kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidationTest.kt diff --git a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt index 54d25ee..099329d 100644 --- a/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt +++ b/integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt @@ -1124,6 +1124,103 @@ class ClientTest { assertEquals(elicitationResultContent, result.content) } + @Test + fun `should reject accepted elicitation content that does not match requested schema`() = runTest { + val requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + required = listOf("name"), + ) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = buildJsonObject { put("name", 42) }) + } + + val exception = assertFailsWith { + serverSession.createElicitation(message = "mismatch", requestedSchema = requestedSchema) + } + assertTrue( + exception.message.orEmpty() + .startsWith("Elicitation response content does not match requested schema: "), + "Unexpected message: ${exception.message}", + ) + assertTrue("'name' must be string" in exception.message.orEmpty(), "Unexpected message: ${exception.message}") + + client.close() + } + + @Test + fun `should reject accepted elicitation without content when schema has required properties`() = runTest { + val requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + required = listOf("name"), + ) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = null) + } + + val exception = assertFailsWith { + serverSession.createElicitation(message = "no content", requestedSchema = requestedSchema) + } + assertTrue( + "must have required property 'name'" in exception.message.orEmpty(), + "Unexpected message: ${exception.message}", + ) + + client.close() + } + + @Test + fun `should accept elicitation without content when schema has no required properties`() = runTest { + val requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + ) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Accept, content = null) + } + + val result = serverSession.createElicitation(message = "no required", requestedSchema = requestedSchema) + + assertEquals(ElicitResult.Action.Accept, result.action) + assertNull(result.content) + + client.close() + } + + @Test + fun `should pass through declined elicitation without validation`() = runTest { + val requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + required = listOf("name"), + ) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Decline) + } + + val result = serverSession.createElicitation(message = "decline", requestedSchema = requestedSchema) + + assertEquals(ElicitResult.Action.Decline, result.action) + assertNull(result.content) + + client.close() + } + + @Test + fun `should pass through cancelled elicitation without validation`() = runTest { + val requestedSchema = ElicitRequestParams.RequestedSchema( + properties = mapOf("name" to StringSchema()), + required = listOf("name"), + ) + val (client, serverSession) = setupElicitationPair { + ElicitResult(action = ElicitResult.Action.Cancel) + } + + val result = serverSession.createElicitation(message = "cancel", requestedSchema = requestedSchema) + + assertEquals(ElicitResult.Action.Cancel, result.action) + assertNull(result.content) + + client.close() + } + @Test fun `should apply elicitation defaults for missing fields in empty content`() = runTest { val schema = defaultsTestSchema() diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt index 343774b..21e0925 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt @@ -10,12 +10,14 @@ import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestURLParams import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification +import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult import io.modelcontextprotocol.kotlin.sdk.types.IncludeContext import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest import io.modelcontextprotocol.kotlin.sdk.types.ListRootsResult import io.modelcontextprotocol.kotlin.sdk.types.LoggingLevel import io.modelcontextprotocol.kotlin.sdk.types.LoggingMessageNotification +import io.modelcontextprotocol.kotlin.sdk.types.McpException import io.modelcontextprotocol.kotlin.sdk.types.PingRequest import io.modelcontextprotocol.kotlin.sdk.types.PromptListChangedNotification import io.modelcontextprotocol.kotlin.sdk.types.Request @@ -95,12 +97,17 @@ public interface ClientConnection { * Sends a message to the client requesting elicitation. * This typically results in a form being displayed to the end user. * + * Accepted content is validated against [requestedSchema]. If content is absent, an empty + * object is used for validation only, so declared `required` properties still apply. The + * returned [ElicitResult] is not modified. + * * @param message The message for the elicitation to display. - * @param requestedSchema The schema requested by the client for the elicitation result. + * @param requestedSchema The schema the server requests for the elicitation result. * Influences the form displayed to the user. * @param options Optional request options. * @return The result of the elicitation request. * @throws IllegalStateException If the server or client does not support elicitation. + * @throws McpException If an accepted response does not match [requestedSchema]. */ public suspend fun createElicitation( message: String, @@ -130,10 +137,15 @@ public interface ClientConnection { * Sends a message to the client requesting elicitation. * This typically results in a form being displayed to the user. * + * For form-mode requests, accepted content is validated against the requested schema. If + * content is absent, an empty object is used for validation only, so declared `required` + * properties still apply. The returned [ElicitResult] is not modified. + * * @param request The elicitation request parameters. * @param options Optional request options. * @return The result of the elicitation request. * @throws IllegalStateException If the server or client does not support elicitation. + * @throws McpException If an accepted form-mode response does not match its requested schema. */ public suspend fun createElicitation(request: ElicitRequest, options: RequestOptions? = null): ElicitResult @@ -256,7 +268,13 @@ internal class ClientConnectionImpl(private val session: ServerSession) : Client } } logger.trace { "ElicitRequest: $request" } - return request(request, options) + val result: ElicitResult = request(request, options) + if (params is ElicitRequestFormParams && result.action == ElicitResult.Action.Accept) { + // Peer-response backstop: accepted form content is client-supplied. Absent content is + // validated as an empty object so declared `required` properties still apply. + validateElicitationContent(params.requestedSchema, result.content ?: EmptyJsonObject) + } + return result } override suspend fun createElicitation( diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidation.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidation.kt new file mode 100644 index 0000000..d976d34 --- /dev/null +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidation.kt @@ -0,0 +1,174 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema +import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema +import io.modelcontextprotocol.kotlin.sdk.types.LegacyTitledEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.PrimitiveSchemaDefinition +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.StringSchema +import io.modelcontextprotocol.kotlin.sdk.types.TitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.TitledSingleSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.longOrNull +import kotlin.math.floor + +/** + * Validates accepted form-mode elicitation [content] against the supported constraints in + * [requestedSchema]: types, `required`, numeric bounds, string lengths, enum membership, and item + * counts. Undeclared properties are permitted. + * [StringSchema.format] values are treated as annotations and are not asserted. + * + * @throws McpException with [RPCError.ErrorCode.INVALID_PARAMS] listing the detected violations + * (at most one per property). + */ +internal fun validateElicitationContent(requestedSchema: ElicitRequestParams.RequestedSchema, content: JsonObject) { + val errors = buildList { + for (name in requestedSchema.required.orEmpty()) { + if (name !in content) add("must have required property '$name'") + } + for ((name, schema) in requestedSchema.properties) { + val value = content[name] ?: continue + checkProperty(name, schema, value)?.let(::add) + } + } + if (errors.isNotEmpty()) { + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Elicitation response content does not match requested schema: ${errors.joinToString(", ")}", + ) + } +} + +private fun checkProperty(name: String, schema: PrimitiveSchemaDefinition, value: JsonElement): String? = + when (schema) { + is StringSchema -> { + val string = stringValue(value) ?: return "'$name' must be string" + val minLength = schema.minLength + val maxLength = schema.maxLength + val length = codePointCount(string) + when { + minLength != null && length < minLength -> + "'$name' must NOT have fewer than $minLength characters" + + maxLength != null && length > maxLength -> + "'$name' must NOT have more than $maxLength characters" + + else -> null + } + } + + is IntegerSchema -> { + val integer = integerValue(value) ?: return "'$name' must be integer" + checkRange(name, integer, schema.minimum?.toLong(), schema.maximum?.toLong()) + } + + is DoubleSchema -> { + val number = doubleValue(value) ?: return "'$name' must be number" + checkRange(name, number, schema.minimum, schema.maximum) + } + + is BooleanSchema -> if (booleanValue(value) == null) "'$name' must be boolean" else null + + is UntitledSingleSelectEnumSchema -> checkEnum(name, value, schema.enumValues) + + is TitledSingleSelectEnumSchema -> checkEnum(name, value, schema.oneOf.map { it.const }) + + is LegacyTitledEnumSchema -> checkEnum(name, value, schema.enumValues) + + is UntitledMultiSelectEnumSchema -> + checkMultiSelect(name, value, schema.items.enumValues, schema.minItems, schema.maxItems) + + is TitledMultiSelectEnumSchema -> + checkMultiSelect(name, value, schema.items.anyOf.map { it.const }, schema.minItems, schema.maxItems) + } + +// Keep Double statically typed so comparisons use IEEE 754 semantics; generic Comparable ordering +// treats -0.0 as less than 0.0. +private fun checkRange( + name: String, + value: Long, + minimum: Long?, + maximum: Long?, +): String? = when { + minimum != null && value < minimum -> "'$name' must be >= $minimum" + maximum != null && value > maximum -> "'$name' must be <= $maximum" + else -> null +} + +private fun checkRange( + name: String, + value: Double, + minimum: Double?, + maximum: Double?, +): String? = when { + minimum != null && value < minimum -> "'$name' must be >= $minimum" + maximum != null && value > maximum -> "'$name' must be <= $maximum" + else -> null +} + +private fun checkEnum(name: String, value: JsonElement, allowed: List ): String? { + val string = stringValue(value) ?: return "'$name' must be string" + return if (string in allowed) null else "'$name' must be equal to one of the allowed values" +} + +private fun checkMultiSelect( + name: String, + value: JsonElement, + allowed: List , + minItems: Int?, + maxItems: Int?, +): String? { + val array = value as? JsonArray ?: return "'$name' must be array" + if (minItems != null && array.size < minItems) return "'$name' must NOT have fewer than $minItems items" + if (maxItems != null && array.size > maxItems) return "'$name' must NOT have more than $maxItems items" + for (element in array) { + val item = stringValue(element) ?: return "'$name' items must be string" + if (item !in allowed) return "'$name' items must be equal to one of the allowed values" + } + return null +} + +// JSON distinguishes the string "42" from the number 42; kotlinx's JsonPrimitive accessors parse +// string content too, so every helper checks isString to preserve that distinction. + +private fun stringValue(value: JsonElement): String? = (value as? JsonPrimitive)?.takeIf { it.isString }?.content + +private fun integerValue(value: JsonElement): Long? { + val primitive = (value as? JsonPrimitive)?.takeIf { !it.isString } ?: return null + primitive.longOrNull?.let { return it } + // JSON Schema's "integer" also matches numbers with a zero fractional part (e.g. 5.0, 1e2). + // Values that do not parse as Long are evaluated using their finite Double representation. + val number = primitive.doubleOrNull ?: return null + return if (number.isFinite() && number == floor(number)) number.toLong() else null +} + +private fun doubleValue(value: JsonElement): Double? = + (value as? JsonPrimitive)?.takeIf { !it.isString }?.doubleOrNull?.takeIf { it.isFinite() } + +private fun booleanValue(value: JsonElement): Boolean? = + (value as? JsonPrimitive)?.takeIf { !it.isString }?.booleanOrNull + +// JSON Schema string lengths count Unicode code points (RFC 8259 characters), not UTF-16 units, +// so a surrogate pair is one character. +private fun codePointCount(string: String): Int { + var count = 0 + var index = 0 + while (index < string.length) { + if (string[index].isHighSurrogate() && index + 1 < string.length && string[index + 1].isLowSurrogate()) { + index++ + } + index++ + count++ + } + return count +} diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt index 880f1cf..f20d3e1 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSession.kt @@ -421,12 +421,17 @@ public open class ServerSession( * Sends a message to the client requesting an elicitation. * This typically results in a form being displayed to the end user. * + * Accepted content is validated against [requestedSchema]. If content is absent, an empty + * object is used for validation only, so declared `required` properties still apply. The + * returned [ElicitResult] is not modified. + * * @param message The message for the elicitation to display. - * @param requestedSchema The schema requested by the client for the elicitation result. + * @param requestedSchema The schema the server requests for the elicitation result. * Influences the form displayed to the user. * @param options Optional request options. * @return The result of the elicitation request. * @throws IllegalStateException If the server or client does not support elicitation. + * @throws McpException If an accepted response does not match [requestedSchema]. */ public suspend fun createElicitation( message: String, diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidationTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidationTest.kt new file mode 100644 index 0000000..b218b47 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ElicitationValidationTest.kt @@ -0,0 +1,438 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema +import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema +import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.EnumOption +import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema +import io.modelcontextprotocol.kotlin.sdk.types.LegacyTitledEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.McpException +import io.modelcontextprotocol.kotlin.sdk.types.McpJson +import io.modelcontextprotocol.kotlin.sdk.types.PrimitiveSchemaDefinition +import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.StringSchema +import io.modelcontextprotocol.kotlin.sdk.types.StringSchemaFormat +import io.modelcontextprotocol.kotlin.sdk.types.TitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.TitledSingleSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema +import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import kotlinx.serialization.json.putJsonObject +import org.junit.jupiter.api.assertDoesNotThrow +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +/** + * Unit tests for [validateElicitationContent]: pure-function checks of accepted form-mode + * content against the requested schema. + * + * End-to-end tests for the `createElicitation` round trip live in the `integration-test` + * module under `io.modelcontextprotocol.kotlin.sdk.client`. + */ +class ElicitationValidationTest { + + private fun schema(vararg properties: Pair , required: List ? = null) = + ElicitRequestParams.RequestedSchema(properties = mapOf(*properties), required = required) + + private fun assertRejected( + schema: ElicitRequestParams.RequestedSchema, + content: JsonObject, + expectedError: String, + ) { + val exception = assertFailsWith { validateElicitationContent(schema, content) } + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code) + val message = exception.message.orEmpty() + assertTrue( + message.startsWith("Elicitation response content does not match requested schema: "), + "Unexpected message prefix: $message", + ) + assertTrue(expectedError in message, "Expected '$expectedError' in: $message") + } + + @Test + fun `validate content matching schema passes`() { + assertDoesNotThrow { + validateElicitationContent( + schema( + "name" to StringSchema(minLength = 1, maxLength = 10), + "age" to IntegerSchema(minimum = 0, maximum = 120), + "score" to DoubleSchema(minimum = 0.0, maximum = 1.0), + "subscribed" to BooleanSchema(), + "color" to UntitledSingleSelectEnumSchema(enumValues = listOf("red", "green")), + "size" to TitledSingleSelectEnumSchema( + oneOf = listOf(EnumOption("s", "Small"), EnumOption("l", "Large")), + ), + "tags" to UntitledMultiSelectEnumSchema( + items = UntitledMultiSelectEnumSchema.Items(enumValues = listOf("a", "b", "c")), + minItems = 1, + maxItems = 2, + ), + required = listOf("name", "age"), + ), + buildJsonObject { + put("name", "octocat") + put("age", 42) + put("score", 0.5) + put("subscribed", true) + put("color", "red") + put("size", "l") + putJsonArray("tags") { add("a") } + }, + ) + } + } + + @Test + fun `validate missing required property fails`() { + assertRejected( + schema("name" to StringSchema(), required = listOf("name")), + JsonObject(emptyMap()), + "must have required property 'name'", + ) + } + + @Test + fun `validate empty content with no required properties passes`() { + assertDoesNotThrow { + validateElicitationContent(schema("name" to StringSchema()), JsonObject(emptyMap())) + } + } + + @Test + fun `validate wrong type for string property fails`() { + assertRejected( + schema("name" to StringSchema()), + buildJsonObject { put("name", 42) }, + "'name' must be string", + ) + } + + @Test + fun `validate string length constraints fail`() { + assertRejected( + schema("name" to StringSchema(minLength = 3)), + buildJsonObject { put("name", "ab") }, + "'name' must NOT have fewer than 3 characters", + ) + assertRejected( + schema("name" to StringSchema(maxLength = 3)), + buildJsonObject { put("name", "abcd") }, + "'name' must NOT have more than 3 characters", + ) + } + + @Test + fun `validate json string is not accepted for integer boolean or number`() { + // JsonPrimitive("42") is the JSON string "42", not the number 42; the validator must not + // parse string content as a number. + assertRejected( + schema("age" to IntegerSchema()), + buildJsonObject { put("age", "42") }, + "'age' must be integer", + ) + assertRejected( + schema("score" to DoubleSchema()), + buildJsonObject { put("score", "1.5") }, + "'score' must be number", + ) + assertRejected( + schema("subscribed" to BooleanSchema()), + buildJsonObject { put("subscribed", "true") }, + "'subscribed' must be boolean", + ) + } + + @Test + fun `validate integer outside minimum and maximum fails`() { + assertRejected( + schema("age" to IntegerSchema(minimum = 0)), + buildJsonObject { put("age", -1) }, + "'age' must be >= 0", + ) + assertRejected( + schema("age" to IntegerSchema(maximum = 120)), + buildJsonObject { put("age", 200) }, + "'age' must be <= 120", + ) + } + + @Test + fun `validate non-integral value for integer property fails`() { + assertRejected( + schema("age" to IntegerSchema()), + buildJsonObject { put("age", 3.5) }, + "'age' must be integer", + ) + } + + @Test + fun `validate integral number forms are accepted for integer property`() { + // JSON Schema's "integer" matches numbers with a zero fractional part. + assertDoesNotThrow { + validateElicitationContent( + schema("age" to IntegerSchema(minimum = 0, maximum = 200)), + buildJsonObject { put("age", 5.0) }, + ) + } + } + + @Test + fun `validate exponent form is accepted for integer property`() { + val content = McpJson.parseToJsonElement("""{"age": 1e2}""") as JsonObject + assertDoesNotThrow { + validateElicitationContent( + schema("age" to IntegerSchema(minimum = 0, maximum = 200)), + content, + ) + } + } + + @Test + fun `validate number outside range fails`() { + assertRejected( + schema("score" to DoubleSchema(maximum = 1.0)), + buildJsonObject { put("score", 1.5) }, + "'score' must be <= 1.0", + ) + } + + @Test + fun `validate integral value is accepted for number property`() { + assertDoesNotThrow { + validateElicitationContent( + schema("score" to DoubleSchema(minimum = 0.0)), + buildJsonObject { put("score", 5) }, + ) + } + } + + @Test + fun `validate string length counts unicode code points`() { + // JSON Schema string length counts RFC 8259 characters: one emoji is one character + // even when it is two UTF-16 units. + assertDoesNotThrow { + validateElicitationContent( + schema("emoji" to StringSchema(maxLength = 1)), + buildJsonObject { put("emoji", "😀") }, + ) + } + assertRejected( + schema("emoji" to StringSchema(minLength = 2)), + buildJsonObject { put("emoji", "😀") }, + "'emoji' must NOT have fewer than 2 characters", + ) + } + + @Test + fun `validate string format is treated as an annotation`() { + assertDoesNotThrow { + validateElicitationContent( + schema("email" to StringSchema(format = StringSchemaFormat.Email)), + buildJsonObject { put("email", "not-an-email") }, + ) + } + } + + @Test + fun `validate negative zero satisfies an inclusive zero bound`() { + // IEEE 754 comparison: -0.0 == 0.0, so an inclusive minimum of 0.0 admits -0.0. + assertDoesNotThrow { + validateElicitationContent( + schema("score" to DoubleSchema(minimum = 0.0)), + buildJsonObject { put("score", -0.0) }, + ) + } + } + + @Test + fun `validate enum value not in allowed set fails`() { + assertRejected( + schema("color" to UntitledSingleSelectEnumSchema(enumValues = listOf("red", "green"))), + buildJsonObject { put("color", "blue") }, + "'color' must be equal to one of the allowed values", + ) + assertRejected( + schema("size" to TitledSingleSelectEnumSchema(oneOf = listOf(EnumOption("s", "Small")))), + buildJsonObject { put("size", "xl") }, + "'size' must be equal to one of the allowed values", + ) + assertRejected( + schema("legacy" to LegacyTitledEnumSchema(enumValues = listOf("a", "b"))), + buildJsonObject { put("legacy", "c") }, + "'legacy' must be equal to one of the allowed values", + ) + } + + @Test + fun `validate multi-select constraints fail`() { + val tags = UntitledMultiSelectEnumSchema( + items = UntitledMultiSelectEnumSchema.Items(enumValues = listOf("a", "b", "c")), + minItems = 1, + maxItems = 2, + ) + assertRejected( + schema("tags" to tags), + buildJsonObject { put("tags", "a") }, + "'tags' must be array", + ) + assertRejected( + schema("tags" to tags), + buildJsonObject { + putJsonArray("tags") { + add("a") + add(42) + } + }, + "'tags' items must be string", + ) + assertRejected( + schema("tags" to tags), + buildJsonObject { + putJsonArray("tags") { + add("a") + add("z") + } + }, + "'tags' items must be equal to one of the allowed values", + ) + assertRejected( + schema("tags" to tags), + buildJsonObject { putJsonArray("tags") {} }, + "'tags' must NOT have fewer than 1 items", + ) + assertRejected( + schema("tags" to tags), + buildJsonObject { + putJsonArray("tags") { + add("a") + add("b") + add("c") + } + }, + "'tags' must NOT have more than 2 items", + ) + } + + @Test + fun `validate titled multi-select membership uses option consts`() { + val sizes = TitledMultiSelectEnumSchema( + items = TitledMultiSelectEnumSchema.Items( + anyOf = listOf(EnumOption("s", "Small"), EnumOption("l", "Large")), + ), + ) + assertDoesNotThrow { + validateElicitationContent( + schema("sizes" to sizes), + buildJsonObject { putJsonArray("sizes") { add("s") } }, + ) + } + assertRejected( + schema("sizes" to sizes), + buildJsonObject { putJsonArray("sizes") { add("Small") } }, + "'sizes' items must be equal to one of the allowed values", + ) + } + + @Test + fun `validate json null is rejected with the type error for each schema kind`() { + // JsonNull is a JsonPrimitive whose content is "null"; it must fail the type check, not + // accidentally parse as a value. + assertRejected( + schema("name" to StringSchema()), + buildJsonObject { put("name", JsonNull) }, + "'name' must be string", + ) + assertRejected( + schema("age" to IntegerSchema()), + buildJsonObject { put("age", JsonNull) }, + "'age' must be integer", + ) + assertRejected( + schema("subscribed" to BooleanSchema()), + buildJsonObject { put("subscribed", JsonNull) }, + "'subscribed' must be boolean", + ) + } + + @Test + fun `validate non-primitive value is rejected for scalar schemas`() { + assertRejected( + schema("name" to StringSchema()), + buildJsonObject { putJsonArray("name") { add("x") } }, + "'name' must be string", + ) + assertRejected( + schema("age" to IntegerSchema()), + buildJsonObject { putJsonObject("age") {} }, + "'age' must be integer", + ) + } + + @Test + fun `validate number below minimum fails`() { + assertRejected( + schema("score" to DoubleSchema(minimum = 0.0)), + buildJsonObject { put("score", -0.5) }, + "'score' must be >= 0.0", + ) + } + + @Test + fun `validate non-finite value for number property fails`() { + // McpJson is lenient, so an unquoted NaN literal can arrive off the wire; it decodes to + // the same primitive as Double.NaN here. + assertRejected( + schema("score" to DoubleSchema(minimum = 0.0, maximum = 1.0)), + buildJsonObject { put("score", Double.NaN) }, + "'score' must be number", + ) + } + + @Test + fun `validate required property not declared in properties is still required`() { + // JSON Schema's `required` is independent of `properties`. + assertRejected( + schema("name" to StringSchema(), required = listOf("name", "consent")), + buildJsonObject { put("name", "octocat") }, + "must have required property 'consent'", + ) + } + + @Test + fun `validate properties not declared in schema pass`() { + // JSON Schema default: without an additionalProperties restriction, undeclared keys are permitted. + assertDoesNotThrow { + validateElicitationContent( + schema("name" to StringSchema(), required = listOf("name")), + buildJsonObject { + put("name", "octocat") + put("undeclared", 42) + }, + ) + } + } + + @Test + fun `validate reports every violation in one error`() { + val exception = assertFailsWith { + validateElicitationContent( + schema( + "name" to StringSchema(), + "age" to IntegerSchema(), + required = listOf("name"), + ), + buildJsonObject { put("age", "old") }, + ) + } + val message = exception.message.orEmpty() + assertTrue("must have required property 'name'" in message, message) + assertTrue("'age' must be integer" in message, message) + } +} From 9ef4f80f85ade12128d4f07b311e93eaf3c244e4 Mon Sep 17 00:00:00 2001 From: Pavel Gorgulov Date: Tue, 21 Jul 2026 18:37:21 +0200 Subject: [PATCH 034/106] fix(server): reject duplicate feature names on registration (#883) Reject duplicate feature registration instead of silently overwriting the existing entry. ## Motivation and Context `FeatureRegistry.add`/`addAll` silently replaced a feature registered under the same key and emitted a spurious `list_changed` notification. Tool/prompt names and resource URIs are unique identifiers in MCP, and the TypeScript SDK already rejects duplicates at registration time (`Tool X is already registered`). - `addTool`/`addPrompt`/`addResource`/`addResourceTemplate` now throw `IllegalArgumentException` on a duplicate name/URI; the existing registration stays intact and no notification is emitted. - `addTools`/`addPrompts`/`addResources` are all-or-nothing: batches with intra-batch duplicates or conflicts with existing keys are rejected without registering anything. - To replace a feature, remove it first (`removeTool` + `addTool`), matching the TypeScript SDK's remove-then-re-register path. No new public API. ## How Has This Been Tested? New `FeatureRegistryTest` unit suite (duplicate rejection, batch atomicity, no notification on rejection, re-add after remove) plus integration tests for tools, prompts, resources, and resource templates. The check also surfaced a real duplicate `addResource` in `AbstractResourceIntegrationTest`, fixed here. `jvmTest` (all modules), `apiCheck`, `ktlintCheck`, and `detekt` pass locally. ## Breaking Changes No public API signature changes (`apiCheck` passes against the existing dump). Behavioral: registering a duplicate name/URI now throws `IllegalArgumentException` instead of silently overwriting; remove the feature first to replace it. ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] 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 --- .../kotlin/AbstractResourceIntegrationTest.kt | 17 --- .../sdk/server/ServerBulkFeaturesTest.kt | 74 ++++++++++++ .../kotlin/sdk/server/ServerPromptsTest.kt | 23 ++++ .../sdk/server/ServerResourceTemplateTest.kt | 19 ++++ .../kotlin/sdk/server/ServerResourcesTest.kt | 21 ++++ .../sdk/server/ServerToolsNotificationTest.kt | 22 ++++ .../kotlin/sdk/server/ServerToolsTest.kt | 39 +++++++ .../kotlin/sdk/server/FeatureRegistry.kt | 40 +++++-- .../kotlin/sdk/server/FeatureRegistryTest.kt | 106 ++++++++++++++++++ 9 files changed, 333 insertions(+), 28 deletions(-) create mode 100644 kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistryTest.kt diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt index da7d85b..ab2a854 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractResourceIntegrationTest.kt @@ -77,23 +77,6 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() { ) } - server.addResource( - uri = testResourceUri, - name = testResourceName, - description = testResourceDescription, - mimeType = "text/plain", - ) { request -> - ReadResourceResult( - contents = listOf( - TextResourceContents( - text = testResourceContent, - uri = request.params.uri, - mimeType = "text/plain", - ), - ), - ) - } - server.addResource( uri = binaryResourceUri, name = binaryResourceName, diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerBulkFeaturesTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerBulkFeaturesTest.kt index 1eb828f..52f6908 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerBulkFeaturesTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerBulkFeaturesTest.kt @@ -1,5 +1,6 @@ package io.modelcontextprotocol.kotlin.sdk.server +import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.collections.shouldHaveSize @@ -57,6 +58,39 @@ class ServerBulkFeaturesTest : AbstractServerFeaturesTest() { tools.map { it.name } shouldContainExactlyInAnyOrder listOf("existing", "new-a", "new-b") } + @Test + fun `addTools should throw and register nothing when a tool is already registered`() = runTest { + server.addTool("existing", "Existing") { CallToolResult(emptyList()) } + + shouldThrow { + server.addTools( + listOf( + RegisteredTool(Tool("new-a", ToolSchema())) { CallToolResult(emptyList()) }, + RegisteredTool(Tool("existing", ToolSchema())) { CallToolResult(emptyList()) }, + ), + ) + } + + val tools = client.listTools().tools + + tools shouldHaveSize 1 + tools.map { it.name } shouldContainExactlyInAnyOrder listOf("existing") + } + + @Test + fun `addTools should throw when batch contains duplicate names`() = runTest { + shouldThrow { + server.addTools( + listOf( + RegisteredTool(Tool("dup", ToolSchema())) { CallToolResult(emptyList()) }, + RegisteredTool(Tool("dup", ToolSchema())) { CallToolResult(emptyList()) }, + ), + ) + } + + client.listTools().tools.shouldBeEmpty() + } + // ── addPrompts ───────────────────────────────────────────────────────────── @Test @@ -89,6 +123,25 @@ class ServerBulkFeaturesTest : AbstractServerFeaturesTest() { prompts.map { it.name } shouldContainExactlyInAnyOrder listOf("existing-prompt", "new-p") } + @Test + fun `addPrompts should throw and register nothing when a prompt is already registered`() = runTest { + server.addPrompt("existing-prompt", "Existing") { GetPromptResult(messages = emptyList()) } + + shouldThrow { + server.addPrompts( + listOf( + RegisteredPrompt(Prompt("new-p")) { GetPromptResult(messages = emptyList()) }, + RegisteredPrompt(Prompt("existing-prompt")) { GetPromptResult(messages = emptyList()) }, + ), + ) + } + + val prompts = client.listPrompts().prompts + + prompts shouldHaveSize 1 + prompts.map { it.name } shouldContainExactlyInAnyOrder listOf("existing-prompt") + } + // ── addResources ─────────────────────────────────────────────────────────── @Test @@ -123,6 +176,27 @@ class ServerBulkFeaturesTest : AbstractServerFeaturesTest() { resources.map { it.uri } shouldContainExactlyInAnyOrder listOf("test://existing-res", "test://new-res") } + @Test + fun `addResources should throw and register nothing when a resource is already registered`() = runTest { + server.addResource("test://existing-res", "Existing", "Existing resource") { + ReadResourceResult(emptyList()) + } + + shouldThrow { + server.addResources( + listOf( + RegisteredResource(Resource("test://new-res", "New")) { ReadResourceResult(emptyList()) }, + RegisteredResource(Resource("test://existing-res", "Dup")) { ReadResourceResult(emptyList()) }, + ), + ) + } + + val resources = client.listResources().resources + + resources shouldHaveSize 1 + resources.map { it.uri } shouldContainExactlyInAnyOrder listOf("test://existing-res") + } + // ── Partial-batch remove ─────────────────────────────────────────────────── @Test diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsTest.kt index d7ae46d..44b1e42 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerPromptsTest.kt @@ -1,5 +1,8 @@ package io.modelcontextprotocol.kotlin.sdk.server +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.Method @@ -110,6 +113,26 @@ class ServerPromptsTest : AbstractServerFeaturesTest() { assertFalse(promptListChangedNotificationReceived, "No notification should be sent when prompt doesn't exist") } + @Test + fun `addPrompt should throw when prompt with same name is already registered`() = runTest { + server.addPrompt(Prompt("test-prompt", "Original Prompt", null)) { + GetPromptResult(description = "original", messages = emptyList()) + } + + val exception = assertThrows { + server.addPrompt(Prompt("test-prompt", "Duplicate Prompt", null)) { + GetPromptResult(description = "duplicate", messages = emptyList()) + } + } + exception.message.orEmpty() shouldContain "Prompt \"test-prompt\" is already registered" + + // The original registration is intact + val prompts = client.listPrompts().prompts + assertEquals(1, prompts.size, "Only the original prompt should be registered") + val result = client.getPrompt(GetPromptRequest(GetPromptRequestParams("test-prompt"))) + assertEquals("original", result.description) + } + @Test fun `removePrompt should throw when prompts capability is not supported`() = runTest { var promptListChangedNotificationReceived = false diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourceTemplateTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourceTemplateTest.kt index c8ace19..8405469 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourceTemplateTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourceTemplateTest.kt @@ -5,6 +5,7 @@ import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.maps.shouldContainKey import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.ListResourceTemplatesRequest import io.modelcontextprotocol.kotlin.sdk.types.McpException @@ -162,6 +163,24 @@ class ServerResourceTemplateTest : AbstractServerFeaturesTest() { removed shouldBe false } + @Test + fun `addResourceTemplate should throw when template with same uriTemplate is already registered`() { + server.addResourceTemplate("test://items/{id}", "Item") { _, _ -> + ReadResourceResult(emptyList()) + } + + val exception = assertThrows { + server.addResourceTemplate("test://items/{id}", "Duplicate Item") { _, _ -> + ReadResourceResult(emptyList()) + } + } + exception.message.orEmpty() shouldContain "ResourceTemplate \"test://items/{id}\" is already registered" + + // The original registration is intact + server.resourceTemplates shouldHaveSize 1 + server.resourceTemplates[0].name shouldBe "Item" + } + @Test fun `addResourceTemplate should throw when resources capability is not supported`() { val noResourcesServer = Server( diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesTest.kt index fafc3c8..a830788 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerResourcesTest.kt @@ -1,7 +1,10 @@ package io.modelcontextprotocol.kotlin.sdk.server +import io.kotest.matchers.string.shouldContain import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest +import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult import io.modelcontextprotocol.kotlin.sdk.types.ResourceListChangedNotification import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities @@ -115,6 +118,24 @@ class ServerResourcesTest : AbstractServerFeaturesTest() { ) } + @Test + fun `addResource should throw when resource with same uri is already registered`() = runTest { + server.addResource("test://resource", "Original", "Original resource") { + ReadResourceResult(listOf(TextResourceContents(text = "original", uri = "test://resource"))) + } + + val exception = assertThrows { + server.addResource("test://resource", "Duplicate", "Duplicate resource") { + ReadResourceResult(listOf(TextResourceContents(text = "duplicate", uri = "test://resource"))) + } + } + exception.message.orEmpty() shouldContain "Resource \"test://resource\" is already registered" + + // The original registration is intact + val result = client.readResource(ReadResourceRequest(ReadResourceRequestParams("test://resource"))) + assertEquals("original", (result.contents.single() as TextResourceContents).text) + } + @Test fun `removeResource should throw when resources capability is not supported`() = runTest { // Create server without resources capability diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsNotificationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsNotificationTest.kt index 3572082..9ac744b 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsNotificationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsNotificationTest.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.test.runTest import org.awaitility.kotlin.await import org.awaitility.kotlin.untilAsserted import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -81,6 +82,27 @@ class ServerToolsNotificationTest : AbstractServerFeaturesTest() { } } + @Test + fun `addTool should not send notification when duplicate is rejected`() = runTest { + // Track notifications + val notifications = mutableListOf () + client.setNotificationHandler ( + Method.Defined.NotificationsToolsListChanged, + ) { notification -> + notifications.add(notification) + CompletableDeferred(Unit) + } + + server.addTool("test-tool", "Test Tool") { CallToolResult(emptyList()) } + assertThrows { + server.addTool("test-tool", "Duplicate Tool") { CallToolResult(emptyList()) } + } + // Close the server to stop processing further events and flush notifications + server.close() + + assertEquals(1, notifications.size, "Only the first registration should send a notification") + } + @Test fun `notification should not be send when removed tool does not exists`() = runTest { // Track notifications diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsTest.kt index 54c998e..1b3df91 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerToolsTest.kt @@ -1,5 +1,8 @@ package io.modelcontextprotocol.kotlin.sdk.server +import io.kotest.matchers.string.shouldContain +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.Method @@ -108,6 +111,42 @@ class ServerToolsTest : AbstractServerFeaturesTest() { assertTrue(result, "Tool with non-conforming name should have been registered and removable") } + @Test + fun `addTool should throw when tool with same name is already registered`() = runTest { + server.addTool("test-tool", "Original Tool") { + CallToolResult(listOf(TextContent("Original result"))) + } + + val exception = assertThrows { + server.addTool("test-tool", "Duplicate Tool") { + CallToolResult(listOf(TextContent("Duplicate result"))) + } + } + exception.message.orEmpty() shouldContain "Tool \"test-tool\" is already registered" + + // The original registration is intact + val tools = client.listTools().tools + assertEquals(1, tools.size, "Only the original tool should be registered") + val result = client.callTool(CallToolRequest(CallToolRequestParams("test-tool"))) + assertEquals("Original result", (result.content.single() as TextContent).text) + } + + @Test + fun `addTool should succeed after removing existing tool`() = runTest { + server.addTool("test-tool", "Original Tool") { + CallToolResult(listOf(TextContent("Original result"))) + } + + server.removeTool("test-tool") + server.addTool("test-tool", "Replacement Tool") { + CallToolResult(listOf(TextContent("Replacement result"))) + } + + assertEquals(1, client.listTools().tools.size, "Re-adding after removal should not duplicate the tool") + val result = client.callTool(CallToolRequest(CallToolRequestParams("test-tool"))) + assertEquals("Replacement result", (result.content.single() as TextContent).text) + } + @Test fun `removeTool should throw when tools capability is not supported`() = runTest { var toolListChangedNotificationReceived = false diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistry.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistry.kt index 0ed6558..142d59c 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistry.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistry.kt @@ -20,9 +20,12 @@ internal interface FeatureListener { * A generic registry for managing features of a specified type. This class provides thread-safe * operations for adding, removing, and retrieving features from the registry. * + * Feature keys are unique: [add] and [addAll] reject keys that are already registered. + * To replace a feature, [remove] it first and add the new one. + * * @param T The type of the feature, constrained to implement the [Feature] interface. * @param featureType A string description of the type of feature being managed. - * Used primarily for logging purposes. + * Used for logging and error messages. */ internal class FeatureRegistry (private val featureType: String) { @@ -44,11 +47,11 @@ internal class FeatureRegistry (private val featureType: String) { private val listeners = atomic(persistentListOf ()) internal fun addListener(listener: FeatureListener) { - listeners.update { it.add(listener) } + listeners.update { it.adding(listener) } } internal fun removeListener(listener: FeatureListener) { - listeners.update { it.remove(listener) } + listeners.update { it.removing(listener) } } /** @@ -58,27 +61,42 @@ internal class FeatureRegistry (private val featureType: String) { */ internal fun add(feature: T) { logger.info { "Adding $featureType: \"${feature.key}\"" } - val oldMap = registry.getAndUpdate { current -> current.put(feature.key, feature) } - val oldFeature = oldMap[feature.key] + registry.update { current -> + require(!current.containsKey(feature.key)) { + "$featureType \"${feature.key}\" is already registered. Remove it first to replace it." + } + current.putting(feature.key, feature) + } logger.info { "Added $featureType: \"${feature.key}\"" } - notifyFeatureUpdated(oldFeature, feature) + notifyFeatureUpdated(null, feature) } /** * Adds the given list of features to the registry. Each feature is mapped by its key - * and added to the current registry. + * and added to the current registry. The operation is all-or-nothing: if any key is + * rejected, no features are added. * * @param features The list of features to add to the registry. */ internal fun addAll(features: List ) { logger.info { "Adding ${featureType}s: ${features.size}" } - val oldMap = registry.getAndUpdate { current -> current.putAll(features.associateBy { it.key }) } + val newEntries = features.associateBy { it.key } + require(newEntries.size == features.size) { + val duplicated = features.groupingBy { it.key }.eachCount().filterValues { it > 1 }.keys + "Duplicate $featureType keys in batch: $duplicated" + } + registry.update { current -> + val conflicting = newEntries.keys.filter { current.containsKey(it) } + require(conflicting.isEmpty()) { + "${featureType}s already registered: $conflicting. Remove them first to replace them." + } + current.puttingAll(newEntries) + } logger.info { "Added ${featureType}s: ${features.size}" } for (feature in features) { - val oldFeature = oldMap[feature.key] - notifyFeatureUpdated(oldFeature, feature) + notifyFeatureUpdated(null, feature) } } @@ -90,7 +108,7 @@ internal class FeatureRegistry (private val featureType: String) { */ internal fun remove(key: FeatureKey): Boolean { logger.info { "Removing $featureType: \"$key\"" } - val oldMap = registry.getAndUpdate { current -> current.remove(key) } + val oldMap = registry.getAndUpdate { current -> current.removing(key) } val removedFeature = oldMap[key] val removed = removedFeature != null diff --git a/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistryTest.kt b/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistryTest.kt new file mode 100644 index 0000000..c6bf72b --- /dev/null +++ b/kotlin-sdk-server/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/FeatureRegistryTest.kt @@ -0,0 +1,106 @@ +package io.modelcontextprotocol.kotlin.sdk.server + +import io.kotest.matchers.shouldBe +import kotlin.test.Test +import kotlin.test.assertFailsWith + +private data class TestFeature(override val key: FeatureKey, val payload: String = "") : Feature + +private class RecordingListener : FeatureListener { + val updatedKeys = mutableListOf () + + override fun onFeatureUpdated(featureKey: String) { + updatedKeys.add(featureKey) + } +} + +class FeatureRegistryTest { + + private val registry = FeatureRegistry ("Tool") + private val listener = RecordingListener() + + init { + registry.addListener(listener) + } + + @Test + fun `add should register feature and notify listener`() { + val feature = TestFeature("a") + + registry.add(feature) + + registry.values shouldBe mapOf("a" to feature) + listener.updatedKeys shouldBe listOf("a") + } + + @Test + fun `add should throw when key is already registered`() { + registry.add(TestFeature("a")) + + val exception = assertFailsWith { + registry.add(TestFeature("a", payload = "override")) + } + + exception.message shouldBe "Tool \"a\" is already registered. Remove it first to replace it." + } + + @Test + fun `add should keep original feature and not notify when duplicate is rejected`() { + val original = TestFeature("a", payload = "original") + registry.add(original) + + assertFailsWith { + registry.add(TestFeature("a", payload = "override")) + } + + registry.values shouldBe mapOf("a" to original) + listener.updatedKeys shouldBe listOf("a") + } + + @Test + fun `addAll should register all features and notify per feature`() { + val a = TestFeature("a") + val b = TestFeature("b") + + registry.addAll(listOf(a, b)) + + registry.values shouldBe mapOf("a" to a, "b" to b) + listener.updatedKeys shouldBe listOf("a", "b") + } + + @Test + fun `addAll should throw on duplicate keys within batch and register nothing`() { + val exception = assertFailsWith { + registry.addAll(listOf(TestFeature("a"), TestFeature("b"), TestFeature("a"))) + } + + exception.message shouldBe "Duplicate Tool keys in batch: [a]" + registry.values shouldBe emptyMap () + listener.updatedKeys shouldBe emptyList () + } + + @Test + fun `addAll should throw when any key is already registered and register nothing`() { + val existing = TestFeature("existing") + registry.add(existing) + + val exception = assertFailsWith { + registry.addAll(listOf(TestFeature("new"), TestFeature("existing", payload = "override"))) + } + + exception.message shouldBe "Tools already registered: [existing]. Remove them first to replace them." + registry.values shouldBe mapOf("existing" to existing) + listener.updatedKeys shouldBe listOf("existing") + } + + @Test + fun `add should succeed after the existing feature is removed`() { + registry.add(TestFeature("a", payload = "original")) + registry.remove("a") + + registry.add(TestFeature("a", payload = "replacement")) + + registry.values shouldBe mapOf("a" to TestFeature("a", payload = "replacement")) + listener.updatedKeys shouldBe listOf("a", "a", "a") + } +} From a7bb0ea73ffd0f5b92ab877caa90a99517eb210f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:46:05 +0200 Subject: [PATCH 035/106] chore(deps): bump actions/setup-node from 6 to 7 in the all-actions group across 1 directory (#898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-actions group with 1 update in the / directory: [actions/setup-node](https://github.com/actions/setup-node). Updates `actions/setup-node` from 6 to 7 Release notes
Sourced from actions/setup-node's releases.
v7.0.0
What's Changed
Enhancements:
- Add cache-primary-key and cache-matched-key as outputs by
@gowridurgadin actions/setup-node#1577- Migrate to ESM and upgrade dependencies by
@gowridurgadin actions/setup-node#1574Bug fixes:
- Remove dummy NODE_AUTH_TOKEN export by
@gowridurgadin actions/setup-node#1558- Only use
mirrorTokeningetManifestif it's provided by@deigain actions/setup-node#1548Documentation updates:
- Add documentation for publishing to npm with Trusted Publisher (OIDC) by
@chiranjib-swainin actions/setup-node#1536- docs: Update restore-only cache documentation by
@priya-kinthaliin actions/setup-node#1550- docs: Update caching recommendations to mitigate cache poisoning risks by
@chiranjib-swainin actions/setup-node#1567Dependency update:
- Upgrade
@actions/cacheto 5.1.0, log cache write denied by@jasonginin actions/setup-node#1569New Contributors
@chiranjib-swainmade their first contribution in actions/setup-node#1536@deigamade their first contribution in actions/setup-node#1548@jasonginmade their first contribution in actions/setup-node#1569Full Changelog: https://github.com/actions/setup-node/compare/v6...v7.0.0
v6.5.0
What's Changed
- Update
@actions/cacheto 5.1.0 and add security overrides for undici and fast-xml-parser by@HarithaVattikutiin actions/setup-node#1579Full Changelog: https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0
v6.4.0
What's Changed
Dependency updates:
- Upgrade
@actionsdependencies by@Copilotin actions/setup-node#1525- Update Node.js versions in versions.yml and bump package to v6.4.0 by
@priya-kinthaliin actions/setup-node#1533New Contributors
@Copilotmade their first contribution in actions/setup-node#1525Full Changelog: https://github.com/actions/setup-node/compare/v6...v6.4.0
v6.3.0
What's Changed
Enhancements:
- Support parsing
devEnginesfield by@susnuxin actions/setup-node#1283... (truncated)
Commits
8207627Migrate to ESM and upgrade dependencies (#1574)04be95cAdd cache-primary-key and cache-matched-key as outputs (#1577)7c2c68ddocs: Update caching recommendations to mitigate cache poisoning risks (#1567)6a61c03Merge pull request #1569 from jasongin/update-actions-cache-5.1.030eb73bResolve high-severity audit issues4e1a87aUpdate dist360237fStrict equality4f8aac5Bump@actions/cacheto 5.1.0, log cache write deniedf4a67bbOnly usemirrorTokeningetManifestif it's provided (#1548)0355742Remove dummy NODE_AUTH_TOKEN export (#1558)- Additional commits viewable in compare view
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/gradle-publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b6e415..147163c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: - name: Set up Node.js if: matrix.os == 'ubuntu-latest' - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index f9df053..058e041 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -25,7 +25,7 @@ jobs: distribution: 'temurin' - name: Set up Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed From 152a1c88ee774f2bd21ff40e587477e5337931f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 01:29:46 +0200 Subject: [PATCH 036/106] chore(deps): bump body-parser from 2.2.2 to 2.3.0 in /integration-test/src/jvmTest/typescript (#907) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [body-parser](https://github.com/expressjs/body-parser) from 2.2.2 to 2.3.0. Release notes
Sourced from body-parser's releases.
v2.3.0
Important: Security
- Security fix for CVE-2026-12590 (GHSA-v422-hmwv-36x6)
What's Changed
- build(deps): bump actions/download-artifact from 6.0.0 to 7.0.0 by
@dependabot[bot] in expressjs/body-parser#681- build(deps): bump actions/checkout from 5.0.0 to 6.0.1 by
@dependabot[bot] in expressjs/body-parser#682- build(deps): bump actions/setup-node from 6.0.0 to 6.1.0 by
@dependabot[bot] in expressjs/body-parser#683- build(deps): bump actions/upload-artifact from 5.0.0 to 6.0.0 by
@dependabot[bot] in expressjs/body-parser#685- build(deps): bump github/codeql-action from 4.31.2 to 4.31.9 by
@dependabot[bot] in expressjs/body-parser#684- perf(urlencoded): move empty-body guard to avoid extra function closure by
@Phillip9587in expressjs/body-parser#647- Improve ESM compatibility by
@Phillip9587in expressjs/body-parser#697- docs: add recommendations for configuring payload limits by
@bjohansebasin expressjs/body-parser#699- build(deps): bump actions/setup-node from 6.1.0 to 6.2.0 by
@dependabot[bot] in expressjs/body-parser#701- build(deps): bump github/codeql-action from 4.31.10 to 4.32.0 by
@dependabot[bot] in expressjs/body-parser#702- build(deps): bump actions/checkout from 6.0.1 to 6.0.2 by
@dependabot[bot] in expressjs/body-parser#700- chore: add explicit type commonjs to package.json by
@Phillip9587in expressjs/body-parser#711- deps: update dependencies to latest versions by
@Phillip9587in expressjs/body-parser#708- build(deps): bump actions/download-artifact from 7.0.0 to 8.0.0 by
@dependabot[bot] in expressjs/body-parser#712- build(deps): bump github/codeql-action from 4.32.0 to 4.32.4 by
@dependabot[bot] in expressjs/body-parser#713- build(deps): bump actions/upload-artifact from 6.0.0 to 7.0.0 by
@dependabot[bot] in expressjs/body-parser#714- fix: improve limit option validation by
@Phillip9587in expressjs/body-parser#698- build(deps): bump github/codeql-action from 4.32.4 to 4.35.1 by
@dependabot[bot] in expressjs/body-parser#719- build(deps): bump actions/setup-node from 6.2.0 to 6.3.0 by
@dependabot[bot] in expressjs/body-parser#718- build(deps): bump actions/download-artifact from 8.0.0 to 8.0.1 by
@dependabot[bot] in expressjs/body-parser#717- perf: eliminate conditional check in json strict mode hot path by
@Phillip9587in expressjs/body-parser#651- ci: add node.js 26 to text matrix by
@Phillip9587in expressjs/body-parser#726- build(deps): bump actions/setup-node from 6.3.0 to 6.4.0 by
@dependabot[bot] in expressjs/body-parser#725- build(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 by
@dependabot[bot] in expressjs/body-parser#724- build(deps): bump github/codeql-action from 4.35.1 to 4.35.3 by
@dependabot[bot] in expressjs/body-parser#723- Upgrade "content-type" by
@blakeembreyin expressjs/body-parser#728- refactor: switch to const/let and enable eslint no-var rule by
@Phillip9587in expressjs/body-parser#729- Update outdated reference to MDN docs by
@krzysdzin expressjs/body-parser#730- build(deps): bump github/codeql-action from 4.35.3 to 4.36.1 by
@dependabot[bot] in expressjs/body-parser#731- build(deps): bump actions/checkout from 6.0.2 to 6.0.3 by
@dependabot[bot] in expressjs/body-parser#732- chore: updated deps to latest by
@Phillip9587in expressjs/body-parser#733- 2.3.0 by
@UlisesGasconin expressjs/body-parser#735New Contributors
@krzysdzmade their first contribution in expressjs/body-parser#730Full Changelog: https://github.com/expressjs/body-parser/compare/v2.2.2...v2.3.0
Changelog
Sourced from body-parser's changelog.
2.3.0 / 2026-06-15
- Security fix for GHSA-v422-hmwv-36x6
- fix: use static exports instead of lazy getters to improve ESM compatibility
- feat: add subpath exports for individual parsers
- fix: improve
limitoption validation (#698)
- Invalid
limitvalues (e.g. unparseable strings orNaN) now throw instead of being silently ignored, which previously disabled size limit enforcementnullandundefinedfall back to the default 100kb limit- deps:
- content-type@^2.0.0
- http-errors@^2.0.1
- iconv-lite^0.7.2
- qs@^6.15.2
- raw-body@^3.0.2
- type-is@^2.1.0
Commits
d0f2ace2.3.0 (#735)7d03f2fchore: updated deps to latest (#733)8024ba7build(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#732)32b4ed4build(deps): bump github/codeql-action from 4.35.3 to 4.36.1 (#731)ff0f6b9docs: update outdated reference to MDN docs (#730)14d001arefactor: switch to const/let and enable eslint no-var rule (#729)37f36a2deps: update content-type and type-is (#728)e1c244bbuild(deps): bump github/codeql-action from 4.35.1 to 4.35.3 (#723)e01087fbuild(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 (#724)a7698d3build(deps): bump actions/setup-node from 6.3.0 to 6.4.0 (#725)- Additional commits viewable in compare view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---Signed-off-by: dependabot[bot]Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot showignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/modelcontextprotocol/kotlin-sdk/network/alerts). Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/jvmTest/typescript/package-lock.json | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index c9d73bf..83b03fb 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -1010,20 +1010,20 @@ } }, "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" @@ -1033,6 +1033,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2039,17 +2052,34 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typescript": { From 031a90c8a107beaaebb7f604d93544ae13a9e23c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:27:20 +0200 Subject: [PATCH 037/106] chore(deps): bump ip-address and express-rate-limit in /integration-test/src/jvmTest/typescript (#906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ip-address](https://github.com/beaugunderson/ip-address) and [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit). These dependencies needed to be updated together. Updates `ip-address` from 10.1.0 to 10.2.0 Commits
80fccaa10.2.0abaeb4dType Address4.addressMinusSuffix as non-nilable (closes #143)2878c29Preserve subnet prefix through Address6.to4() (closes #123) (#203)586666eReject trailing junk in Address6.fromURL (closes #158) (#202)80bc76eValidate static factories instead of silently overflowing (#201)98927beClarify isValid() accepts CIDRs with host bits set (#81)a0eb073Fix getScope() and broaden getType() classification (closes #122) (#200)ec52105Add networkForm() for CIDR network-address strings (#199)a9443a7Add isMapped4() predicate for IPv4-mapped IPv6 addresses (closes #62) (#198)f01d742Add address-property predicates (private, ULA, loopback, link-local, etc.) (#...- Additional commits viewable in compare view
Updates `express-rate-limit` from 8.3.1 to 8.6.0Release notes
Sourced from express-rate-limit's releases.
v8.6.0
You can view the changelog here.
v8.5.2
You can view the changelog here.
v8.5.1
You can view the changelog here.
v8.5.0
You can view the changelog here.
v8.4.1
You can view the changelog here.
v8.4.0
You can view the changelog here.
v8.3.2
You can view the changelog here.
Commits
fffb3c48.6.0f366b2ddocs: debugging guide, time constants, & v8.6.0 changelog (#652)593ddd2fix: make debug output easier to read (#653)ef8c129fix: Pin safe version of@asyncapi/specsdev dep (#659)7b05e0dfeat: add time constants to support more readable values for windowMs (#655)863e730chore(deps-dev): bump the development-dependencies group with 3 updates (#657)e0e711efix: correct wording in usage documentation for express-rate-limit (#656)fcd3aa7chore(deps-dev): bump the development-dependencies group with 3 updates (#651)99d4298feat: usedebugfor debug logging (#641)23e4ddefeat: Run validations once each (#650)- Additional commits viewable in compare view
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../src/jvmTest/typescript/package-lock.json | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index 83b03fb..b778fbb 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -1362,12 +1362,13 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", - "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz", + "integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "debug": "^4.4.3", + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -1588,9 +1589,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" From 9308473745711b40784cdbc89b6f7ada64c550bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:26:55 +0200 Subject: [PATCH 038/106] chore(deps): bump fast-uri from 3.1.2 to 3.1.4 in /integration-test/src/jvmTest/typescript (#910) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.2 to 3.1.4. Release notes
Sourced from fast-uri's releases.
v3.1.4
⚠️ Security Release
Fix for https://github.com/fastify/fast-uri/security/advisories/GHSA-v2hh-gcrm-f6hx
Full Changelog: https://github.com/fastify/fast-uri/compare/v3.1.3...v3.1.4
v3.1.3
⚠️ Security Release
Full Changelog: https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.3
Commits
6aeece6Bumped v3.1.42d50fbafix: reject literal backslash in URI authority0549fe3Bumped v3.1.32a6d357Merge commit from fork- See full diff in compare view
Signed-off-by: dependabot[bot]Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- integration-test/src/jvmTest/typescript/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json index b778fbb..0c70662 100644 --- a/integration-test/src/jvmTest/typescript/package-lock.json +++ b/integration-test/src/jvmTest/typescript/package-lock.json @@ -1387,9 +1387,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "funding": [ { "type": "github", From b6cb547d582a89c4e51ac4ae4e29ab1d53dd7ff3 Mon Sep 17 00:00:00 2001 From: King Star Date: Wed, 22 Jul 2026 21:53:01 +0800 Subject: [PATCH 039/106] fix(server): preserve id on duplicate initialize errors (#868) ## Summary Fixes #866. ## Changes - Preserve the parsed JSON-RPC request id when Streamable HTTP rejects a duplicate `initialize` request. - Keep pre-parse and transport-level rejection paths unchanged when the request id is not available. - Extend the duplicate-initialize regression test to assert the error response id as well as the HTTP status. ## Verification ```bash ./gradlew :kotlin-sdk-server:jvmTest --tests 'io.modelcontextprotocol.kotlin.sdk.server.StreamableHttpServerTransportTest.second initialization request returns JSON-RPC error with request id' ./gradlew :kotlin-sdk-server:jvmTest --tests 'io.modelcontextprotocol.kotlin.sdk.server.StreamableHttpServerTransportTest' ./gradlew :kotlin-sdk-server:jvmTest ./gradlew :kotlin-sdk-server:ktlintCheck :kotlin-sdk-server:detekt ``` Co-authored-by: Pavel Gorgulov --- .../server/StreamableHttpServerTransport.kt | 18 ++++++++++++------ .../StreamableHttpServerTransportTest.kt | 9 +++++++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt index 1f8ac39..3b02835 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt @@ -459,9 +459,10 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } val messages = parseBody(call) ?: return - val isInitializationRequest = messages.any { - it is JSONRPCRequest && it.method == Method.Defined.Initialize.value + val initializationRequest = messages.filterIsInstance ().firstOrNull { + it.method == Method.Defined.Initialize.value } + val isInitializationRequest = initializationRequest != null if (isInitializationRequest) { if (initialized.load() && sessionId != null) { @@ -469,6 +470,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat HttpStatusCode.BadRequest, RPCError.ErrorCode.INVALID_REQUEST, "Invalid Request: Server already initialized", + initializationRequest.id, ) return } @@ -506,8 +508,7 @@ public class StreamableHttpServerTransport(private val configuration: Configurat // For initialize requests, get from request params. // For other requests, get from header (already validated). val clientProtocolVersion = if (isInitializationRequest) { - val initRequest = messages.first() as JSONRPCRequest - (initRequest.params as? JsonObject)?.get("protocolVersion") + (initializationRequest.params as? JsonObject)?.get("protocolVersion") ?.let { McpJson.decodeFromJsonElement (it) } ?: DEFAULT_NEGOTIATED_PROTOCOL_VERSION } else { @@ -913,7 +914,12 @@ public class StreamableHttpServerTransport(private val configuration: Configurat } } -internal suspend fun ApplicationCall.reject(status: HttpStatusCode, code: Int, message: String) { +internal suspend fun ApplicationCall.reject( + status: HttpStatusCode, + code: Int, + message: String, + id: RequestId? = null, +) { this.response.status(status) - this.respond(JSONRPCError(id = null, error = RPCError(code = code, message = message))) + this.respond(JSONRPCError(id = id, error = RPCError(code = code, message = message))) } diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt index 172889a..0d99f51 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt @@ -38,6 +38,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse @@ -163,7 +164,7 @@ class StreamableHttpServerTransportTest { } @Test - fun `second initialization request returns an HTTP error`() = testApplication { + fun `second initialization request returns JSON-RPC error with request id`() = testApplication { configTestServer() val client = createTestClient() @@ -186,13 +187,17 @@ class StreamableHttpServerTransportTest { firstResponse.status shouldBe HttpStatusCode.OK + val secondRequest = buildInitializeRequestPayload().copy(id = RequestId("second-init")) val secondResponse = client.post(path) { addStreamableHeaders() header("mcp-session-id", firstResponse.headers[MCP_SESSION_ID_HEADER]) - setBody(payload) + setBody(secondRequest) } secondResponse.status shouldBe HttpStatusCode.BadRequest + val error = secondResponse.body () + error.id shouldBe secondRequest.id + error.error.message shouldBe "Invalid Request: Server already initialized" } @Test From cab669b108b0297201e08f5bd703f4e2476c9525 Mon Sep 17 00:00:00 2001 From: King Star Date: Thu, 23 Jul 2026 00:12:12 +0800 Subject: [PATCH 040/106] fix: classify malformed request params as invalid params (#886) ## Summary Classify typed request deserialization failures as JSON-RPC `Invalid Params` instead of `Internal error`. Fixes #830. ## Changes - translate `SerializationException` from typed request decoding to `-32602` - return a stable `Invalid params` message without serialization internals - cover malformed `initialize` requests through session and Streamable HTTP paths ## Scope and risk The catch is limited to request parameter deserialization after the JSON-RPC method has been resolved. Envelope parsing, unknown methods, handler failures, and successful requests keep their existing behavior. ## Verification ```bash ./gradlew :integration-test:jvmTest --tests 'io.modelcontextprotocol.kotlin.sdk.server.ServerSessionInitializeTest' ./gradlew :kotlin-sdk-server:jvmTest --tests 'io.modelcontextprotocol.kotlin.sdk.server.StreamableHttpServerTransportTest' ./gradlew :kotlin-sdk-core:jvmTest --tests 'io.modelcontextprotocol.kotlin.sdk.shared.ProtocolTest' ./gradlew ktlintCheck apiCheck ``` Co-authored-by: Pavel Gorgulov --- .../sdk/server/ServerSessionInitializeTest.kt | 40 +++++++++++++++++++ .../kotlin/sdk/shared/Protocol.kt | 11 ++++- .../StreamableHttpServerTransportTest.kt | 31 ++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt index 82cf91a..38b169a 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt @@ -7,9 +7,11 @@ import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCError import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION import io.modelcontextprotocol.kotlin.sdk.types.RPCError +import io.modelcontextprotocol.kotlin.sdk.types.RequestId import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.toJSON import kotlinx.coroutines.CompletableDeferred @@ -18,9 +20,13 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject import org.junit.jupiter.api.Test import java.util.concurrent.CopyOnWriteArrayList import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -41,6 +47,18 @@ class ServerSessionInitializeTest { ), ) + private fun createMalformedInitializeRequest(): JSONRPCRequest = JSONRPCRequest( + id = RequestId(1), + method = "initialize", + params = buildJsonObject { + putJsonObject("capabilities") {} + putJsonObject("clientInfo") { + put("name", "repro") + put("version", "0.1.0") + } + }, + ) + @Test fun `should handle first initialize request successfully`() = runTest { val session = createSession() @@ -67,6 +85,28 @@ class ServerSessionInitializeTest { // Both initialize requests arrive before notifications/initialized, in the serial dispatch // phase, so processing and response order stay deterministic. + @Test + fun `should classify malformed initialize params as invalid params`() = runTest { + val session = createSession() + val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() + + val responseDone = CompletableDeferred () + clientTransport.onMessage { message -> + if (message is JSONRPCError) { + responseDone.complete(message) + } + } + + session.connect(serverTransport) + clientTransport.send(createMalformedInitializeRequest()) + + val response = responseDone.await() + assertEquals(RPCError.ErrorCode.INVALID_PARAMS, response.error.code) + assertFalse(response.error.message.contains("kotlinx.serialization")) + assertNull(session.clientCapabilities) + assertNull(session.clientVersion) + } + @Test fun `should reject duplicate initialize request`() = runTest { val session = createSession() diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt index 0c82f5e..908e524 100644 --- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt +++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt @@ -48,6 +48,7 @@ import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.encodeToJsonElement @@ -941,7 +942,15 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio _requestHandlers.update { current -> current.putting(method.value) { jSONRPCRequest, extraHandler -> - val request = jSONRPCRequest.fromJSON() + val request = try { + jSONRPCRequest.fromJSON() + } catch (cause: SerializationException) { + throw McpException( + code = RPCError.ErrorCode.INVALID_PARAMS, + message = "Invalid params", + cause = cause, + ) + } val response = wrapped(request as T, extraHandler) response } diff --git a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt index 0d99f51..bb522d6 100644 --- a/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt +++ b/kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransportTest.kt @@ -47,6 +47,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult import io.modelcontextprotocol.kotlin.sdk.types.McpJson import io.modelcontextprotocol.kotlin.sdk.types.Method +import io.modelcontextprotocol.kotlin.sdk.types.RPCError import io.modelcontextprotocol.kotlin.sdk.types.RequestId import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.types.Tool @@ -225,6 +226,36 @@ class StreamableHttpServerTransportTest { initResponse.headers[MCP_SESSION_ID_HEADER] shouldBe null } + @Test + fun `malformed initialize params return invalid params over streamable http`() = testApplication { + val mcpPath = "/mcp" + + application { + mcpStreamableHttp(mcpPath, enableDnsRebindingProtection = false) { + Server( + Implementation("test-server", "1.0.0"), + ServerOptions(capabilities = ServerCapabilities()), + ) + } + } + + val client = createTestClient() + + val response = client.post(mcpPath) { + addStreamableHeaders() + setBody( + """ + {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"clientInfo":{"name":"repro","version":"0.1.0"}}} + """.trimIndent(), + ) + } + + response.status shouldBe HttpStatusCode.OK + val error = response.body () + error.error.code shouldBe RPCError.ErrorCode.INVALID_PARAMS + error.error.message.contains("kotlinx.serialization") shouldBe false + } + @Test fun `request with unsupported protocol version returns an HTTP error`() = testApplication { configTestServer() From a06d699d6c9dfdf8321e5cbb3f5aebcb0016b6f6 Mon Sep 17 00:00:00 2001 From: UnscientificJsZhai <73090853+UnscientificJsZhai@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:37:37 +0800 Subject: [PATCH 041/106] feat: SSE heartbeat (#761) ## Summary Adds optional SSE heartbeat configuration for streamable HTTP MCP server connections. This lets `Application.mcpStreamableHttp` callers pass Ktor's `Heartbeat` configuration for SSE streams, including heartbeat period and event payload. Heartbeats remain disabled by default, preserving the existing stream behavior unless the new option is explicitly provided. ## Motivation and Context Some MCP clients need heartbeat messages on SSE connections to keep the connection alive. Without those heartbeats, the client may proactively disconnect from the MCP server. MCP server developers need a way to configure an SSE heartbeat mechanism so they can avoid unexpected client disconnects. In my case, my MCP server was consistently disconnected by Gemini CLI. I worked around the issue with some fallback approaches, but I believe the best implementation is to add heartbeat support by using Ktor's built-in API. ## Implementation Notes - Added a function overload of `Application.mcpStreamableHttp` to introduce heartbeat parameter `sseHeartbeatConfig: (Heartbeat.() -> Unit)?`. - Applied the configuration inside the Ktor `sse { ... }` block before handling the existing streamable HTTP transport request. - Updated the server API dump for the new public API surface. ## How Has This Been Tested? Added `StreamableHttpHeartbeatIntegrationTest` covering: - Configured GET SSE streams apply the provided heartbeat configuration. - GET SSE streams do not send default heartbeats when `sseHeartbeatConfig` is omitted. - Configured GET SSE streams emit heartbeat events repeatedly. - Legacy MCP over SSE APIs do not expose the new heartbeat configuration. Verified locally with: ```bash ./gradlew :integration-test:jvmTest --tests "io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp.StreamableHttpHeartbeatIntegrationTest" ``` Result: `BUILD SUCCESSFUL`. ## Breaking Changes None. This change adds a new `Application.mcpStreamableHttp` overload that accepts `sseHeartbeatConfig: (Heartbeat.() -> Unit)?` while keeping the existing overload unchanged. Existing callers continue to compile and keep the previous behavior: SSE heartbeats are not sent unless the new overload is used with an explicit heartbeat configuration. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context Heartbeat behavior is delegated to Ktor's SSE heartbeat support, so applications can use the same configuration semantics they would use in native Ktor SSE routes. Related issue: [SSE needs a heartbeat #344 ](https://github.com/modelcontextprotocol/kotlin-sdk/issues/344) --------- Co-authored-by: Pavel Gorgulov --- .../AbstractStreamableHttpIntegrationTest.kt | 8 +- .../StreamableHttpHeartbeatIntegrationTest.kt | 174 ++++++++++++++++++ kotlin-sdk-server/api/kotlin-sdk-server.api | 4 +- .../kotlin/sdk/server/KtorServer.kt | 7 + 4 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpHeartbeatIntegrationTest.kt diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/AbstractStreamableHttpIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/AbstractStreamableHttpIntegrationTest.kt index 9bc0c17..535393e 100644 --- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/AbstractStreamableHttpIntegrationTest.kt +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/AbstractStreamableHttpIntegrationTest.kt @@ -3,6 +3,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp import io.ktor.server.cio.CIOApplicationEngine import io.ktor.server.engine.EmbeddedServer import io.ktor.server.engine.embeddedServer +import io.ktor.server.sse.Heartbeat import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp @@ -17,7 +18,10 @@ import io.ktor.server.cio.CIO as ServerCIO open class AbstractStreamableHttpIntegrationTest { - suspend fun initTestServer(name: String? = null): StreamableHttpTestServer { + suspend fun initTestServer( + name: String? = null, + sseHeartbeatConfig: (Heartbeat.() -> Unit)? = null, + ): StreamableHttpTestServer { val mcpServer = Server( Implementation(name = name ?: DEFAULT_SERVER_NAME, version = VERSION), ServerOptions( @@ -52,7 +56,7 @@ open class AbstractStreamableHttpIntegrationTest { host = URL, port = PORT, ) { - mcpStreamableHttp { mcpServer } + mcpStreamableHttp(sseHeartbeatConfig = sseHeartbeatConfig) { mcpServer } } return StreamableHttpTestServer( diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpHeartbeatIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpHeartbeatIntegrationTest.kt new file mode 100644 index 0000000..8019557 --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpHeartbeatIntegrationTest.kt @@ -0,0 +1,174 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp + +import io.kotest.matchers.shouldBe +import io.ktor.client.HttpClient +import io.ktor.client.plugins.sse.SSE +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.prepareGet +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.sse.ServerSentEvent +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.readUTF8Line +import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.Implementation +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest +import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequestParams +import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest +import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION +import io.modelcontextprotocol.kotlin.sdk.types.toJSON +import io.modelcontextprotocol.kotlin.test.utils.actualPort +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.time.Duration.Companion.milliseconds +import io.ktor.client.engine.cio.CIO as ClientCIO + +private const val SESSION_ID_HEADER = "mcp-session-id" +private const val PROTOCOL_VERSION_HEADER = "mcp-protocol-version" +private const val DEFAULT_HEARTBEAT_LINE = ": heartbeat" +private const val CUSTOM_HEARTBEAT_LINE = ": mcp-heartbeat" + +class StreamableHttpHeartbeatIntegrationTest : AbstractStreamableHttpIntegrationTest() { + + @Test + fun `GET SSE stream emits configured heartbeat`(): Unit = runBlocking(Dispatchers.IO) { + var server: StreamableHttpTestServer? = null + var httpClient: HttpClient? = null + + try { + server = initTestServer("heartbeat-test") { + period = 50.milliseconds + event = ServerSentEvent(comments = "mcp-heartbeat") + } + val mcpUrl = "http://$URL:${server.ktorServer.actualPort()}/mcp" + httpClient = HttpClient(ClientCIO) { install(SSE) } + val sessionId = initializeSession(httpClient, mcpUrl) + + httpClient.prepareGet(mcpUrl) { + addSseHeaders(sessionId) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.headers[SESSION_ID_HEADER] shouldBe sessionId + + response.bodyAsChannel().readLineMatching(CUSTOM_HEARTBEAT_LINE) shouldBe CUSTOM_HEARTBEAT_LINE + } + } finally { + httpClient?.close() + server?.ktorServer?.stopSuspend(1000, 2000) + } + } + + @Test + fun `GET SSE stream does not emit heartbeat by default`(): Unit = runBlocking(Dispatchers.IO) { + var server: StreamableHttpTestServer? = null + var httpClient: HttpClient? = null + + try { + server = initTestServer("no-heartbeat-test") + val mcpUrl = "http://$URL:${server.ktorServer.actualPort()}/mcp" + httpClient = HttpClient(ClientCIO) { install(SSE) } + val sessionId = initializeSession(httpClient, mcpUrl) + + httpClient.prepareGet(mcpUrl) { + addSseHeaders(sessionId) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.headers[SESSION_ID_HEADER] shouldBe sessionId + + response.bodyAsChannel().readLineMatching(timeoutMillis = 150) { line -> + line.isHeartbeatSseLine() + } shouldBe null + } + } finally { + httpClient?.close() + server?.ktorServer?.stopSuspend(1000, 2000) + } + } + + @Test + fun `GET SSE stream emits configured heartbeat repeatedly`(): Unit = runBlocking(Dispatchers.IO) { + var server: StreamableHttpTestServer? = null + var httpClient: HttpClient? = null + + try { + server = initTestServer("repeating-heartbeat-test") { + period = 50.milliseconds + event = ServerSentEvent(comments = "mcp-heartbeat") + } + val mcpUrl = "http://$URL:${server.ktorServer.actualPort()}/mcp" + httpClient = HttpClient(ClientCIO) { install(SSE) } + val sessionId = initializeSession(httpClient, mcpUrl) + + httpClient.prepareGet(mcpUrl) { + addSseHeaders(sessionId) + }.execute { response -> + response.status shouldBe HttpStatusCode.OK + response.headers[SESSION_ID_HEADER] shouldBe sessionId + + val channel = response.bodyAsChannel() + channel.readLineMatching(CUSTOM_HEARTBEAT_LINE) shouldBe CUSTOM_HEARTBEAT_LINE + channel.readLineMatching(CUSTOM_HEARTBEAT_LINE) shouldBe CUSTOM_HEARTBEAT_LINE + } + } finally { + httpClient?.close() + server?.ktorServer?.stopSuspend(1000, 2000) + } + } + + private suspend fun initializeSession(client: HttpClient, mcpUrl: String): String { + val response = client.post(mcpUrl) { + contentType(ContentType.Application.Json) + header( + HttpHeaders.Accept, + "${ContentType.Application.Json}, ${ContentType.Text.EventStream}", + ) + setBody(Json.encodeToString(buildInitPayload())) + } + + response.status shouldBe HttpStatusCode.OK + return assertNotNull(response.headers[SESSION_ID_HEADER]) + } + + private fun buildInitPayload(): JSONRPCRequest = InitializeRequest( + InitializeRequestParams( + protocolVersion = LATEST_PROTOCOL_VERSION, + capabilities = ClientCapabilities(), + clientInfo = Implementation(name = "heartbeat-test-client", version = "1.0.0"), + ), + ).toJSON() + + private fun io.ktor.client.request.HttpRequestBuilder.addSseHeaders(sessionId: String) { + header(HttpHeaders.Accept, ContentType.Text.EventStream.toString()) + header(SESSION_ID_HEADER, sessionId) + header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION) + } + + private suspend fun ByteReadChannel.readLineMatching(expectedLine: String, timeoutMillis: Long = 2_000): String? = + readLineMatching(timeoutMillis) { line -> line == expectedLine } + + private suspend fun ByteReadChannel.readLineMatching( + timeoutMillis: Long = 2_000, + matches: (String) -> Boolean, + ): String? = withTimeoutOrNull(timeoutMillis.milliseconds) { + var line = readUTF8Line() + while (line != null) { + if (matches(line)) return@withTimeoutOrNull line + line = readUTF8Line() + } + null + } + + private fun String.isHeartbeatSseLine(): Boolean = this == DEFAULT_HEARTBEAT_LINE || + this == CUSTOM_HEARTBEAT_LINE || + (startsWith(":") && contains("heartbeat", ignoreCase = true)) || + (startsWith("event:") && contains("heartbeat", ignoreCase = true)) +} diff --git a/kotlin-sdk-server/api/kotlin-sdk-server.api b/kotlin-sdk-server/api/kotlin-sdk-server.api index 51ac279..1ec2efd 100644 --- a/kotlin-sdk-server/api/kotlin-sdk-server.api +++ b/kotlin-sdk-server/api/kotlin-sdk-server.api @@ -59,8 +59,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt { public static synthetic fun mcp$default (Lio/ktor/server/routing/Route;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public static final fun mcpStatelessStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;)V public static synthetic fun mcpStatelessStreamableHttp$default (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V - public static final fun mcpStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;)V - public static synthetic fun mcpStreamableHttp$default (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static final fun mcpStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun mcpStreamableHttp$default (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V } public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt : io/modelcontextprotocol/kotlin/sdk/server/Feature { diff --git a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt index 73ef258..5fc3d22 100644 --- a/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt +++ b/kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/KtorServer.kt @@ -20,8 +20,10 @@ import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.route import io.ktor.server.routing.routing +import io.ktor.server.sse.Heartbeat import io.ktor.server.sse.SSE import io.ktor.server.sse.ServerSSESession +import io.ktor.server.sse.heartbeat import io.ktor.server.sse.sse import io.ktor.utils.io.KtorDsl import io.modelcontextprotocol.kotlin.sdk.types.RPCError @@ -162,6 +164,7 @@ private fun Application.mcpStreamableHttp( allowedHosts: List