diff --git a/.claude/skills/kdoc/SKILL.md b/.claude/skills/kdoc/SKILL.md
index 4760293..cfacfc3 100644
--- a/.claude/skills/kdoc/SKILL.md
+++ b/.claude/skills/kdoc/SKILL.md
@@ -5,40 +5,62 @@ description: "Add KDoc documentation to Kotlin public API. Use whenever the user
# KDoc Generator
-Add KDoc comments to public Kotlin API declarations.
+Add KDoc to all public Kotlin declarations (classes, interfaces, objects, functions, properties, enums, type aliases, annotations — including `@Deprecated`). Read the implementation, not just the signature, to write accurate descriptions.
-## What to document
+## Tag placement
-All public declarations (no explicit `private`/`internal`/`protected`):
-- Classes, interfaces, objects, sealed classes/interfaces
-- Functions, extension functions
-- Properties
-- Enum classes and entries
-- Type aliases, annotation classes
+| Where the declaration lives | Tag |
+|---|---|
+| Primary-ctor `val` / `var` (public, internal, or protected) | `@property` in class KDoc |
+| Primary-ctor `name: Type` (no `val`/`var`) | `@param` in class KDoc |
+| Primary-ctor `private val` / `private var` | `@param` in class KDoc |
+| Body of class/object/interface/sealed interface | inline `/** */` above the declaration |
+| Function parameter | `@param` |
+| Non-Unit return type | `@return` |
-Include `@Deprecated` elements.
+## Style
-## Context
+- Summary: one concise sentence, third-person verb ("Creates", "Returns", "Represents"). Expand to 2-3 sentences only when genuinely complex.
+- **Example block**: mandatory for DSL builders (show full receiver-scope usage); add for complex functions or non-obvious extension functions; skip for one-liners and simple getters.
+- KDoc links (`[ClassName]`, `[methodName]`) only where they add navigational value.
+- Don't write `@throws` or "suspend" notes.
+- Rewrite existing KDoc if incomplete or low quality.
-Read the implementation, not just the signature, to write accurate descriptions. Understanding what the code actually does prevents superficial or misleading documentation.
+## Examples
-## KDoc format
-
-**Class/interface example:**
+**Class with mixed primary-constructor parameters:**
````kotlin
/**
* Manages active client sessions and their lifecycle.
*
- * Sessions are created on first connection and cleaned up
- * when the transport closes.
- *
* @property maxSessions upper limit on concurrent sessions
- * @property timeout idle timeout before a session is evicted
+ * @param scope coroutine scope for session lifecycle tasks
+ * @param secret used internally for session token signing
*/
+public class SessionManager(
+ public val maxSessions: Int, // public val → @property
+ scope: CoroutineScope, // no val/var → @param
+ private val secret: ByteArray, // private val → @param
+)
````
-**Function example:**
+**Body and interface properties (inline `/** */`):**
+
+````kotlin
+/** Represents an entity that includes additional metadata. */
+public interface WithMeta {
+ /** Optional metadata attached to this entity. */
+ public val meta: JsonObject?
+}
+
+public class Connection(public val id: String) {
+ /** Capabilities negotiated during the handshake, or `null` before initialize completes. */
+ public val capabilities: ServerCapabilities? get() = ...
+}
+````
+
+**Function with Example block:**
````kotlin
/**
@@ -46,10 +68,7 @@ Read the implementation, not just the signature, to write accurate descriptions.
*
* Example:
* ```kotlin
- * server.addTool(
- * name = "echo",
- * description = "Echoes input back",
- * ) { request ->
+ * server.addTool(name = "echo", description = "Echoes input back") { request ->
* CallToolResult(content = listOf(TextContent("Echo: ${request.arguments}")))
* }
* ```
@@ -60,15 +79,3 @@ Read the implementation, not just the signature, to write accurate descriptions.
* @return the registered tool definition
*/
````
-
-## Rules
-
-- Summary: concise first sentence starting with a third-person verb ("Creates", "Returns", "Represents"). Expand to 2-3 sentences only when genuinely complex
-- **@property** in class-level KDoc for all public properties (never as individual KDoc on the property); **@param** for function parameters
-- **@return** for non-Unit return types
-- **Example block**: add for DSL builders, complex functions, extension functions with non-obvious usage. Skip for trivial one-liners and simple getters
-- **DSL builders**: always include an Example showing full usage with the receiver scope — this is critical for discoverability
-- KDoc links (`[ClassName]`, `[methodName]`): only where it adds clear navigational value
-- No **@throws** — don't document exceptions
-- No **suspend** notes — coroutine nature is visible from the signature
-- **Existing KDoc**: rewrite if incomplete (missing @param/@return/@property) or low quality
\ No newline at end of file
diff --git a/.github/workflows/apidocs.yaml b/.github/workflows/apidocs.yaml
index 76f4434..67770a7 100644
--- a/.github/workflows/apidocs.yaml
+++ b/.github/workflows/apidocs.yaml
@@ -24,24 +24,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up JDK ${{ env.JAVA_VERSION }}
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: ${{ env.JAVA_DISTRIBUTION }}
cache: gradle
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v6
+ uses: gradle/actions/setup-gradle@v6.3.0
- name: Generate Dokka Site
run: |-
./gradlew knit docs:classes docs:knitCheck dokkaGenerate
- name: Upload artifact
- uses: actions/upload-pages-artifact@v4
+ uses: actions/upload-pages-artifact@v5
with:
path: build/dokka/html
diff --git a/.github/workflows/automations.yml b/.github/workflows/automations.yml
index 4f88d37..60a9d3b 100644
--- a/.github/workflows/automations.yml
+++ b/.github/workflows/automations.yml
@@ -27,7 +27,7 @@ jobs:
name: Mark/unmark stale PRs and issues
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v10
+ - uses: actions/stale@v11
with:
debug-only: ${{ env.DRY_RUN }}
exempt-draft-pr: true
@@ -66,7 +66,7 @@ jobs:
name: Close stale PRs and issues
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v10
+ - uses: actions/stale@v11
with:
debug-only: ${{ env.DRY_RUN }}
only-labels: 'stale'
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 08242f7..ff99581 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -34,7 +34,7 @@ jobs:
include:
- os: ubuntu-latest
job-name: "Linux Tests"
- gradle-tasks: "ktlintCheck build"
+ gradle-tasks: "build"
max-workers: 3
- os: macos-latest
@@ -43,17 +43,17 @@ jobs:
max-workers: 2
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up JDK ${{ env.JAVA_VERSION }}
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: ${{ env.JAVA_DISTRIBUTION }}
- 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
@@ -63,7 +63,7 @@ jobs:
run: npm ci
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v6
+ uses: gradle/actions/setup-gradle@v6.3.0
with:
validate-wrappers: true
add-job-summary: 'always'
@@ -104,22 +104,6 @@ jobs:
report_paths: '**/test-results/**/TEST-*.xml'
truncate_stack_traces: false
- - name: Upload coverage reports to Codecov
- if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }}
- uses: codecov/codecov-action@v6
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- report_type: coverage
- directory: ./build/reports/kover
- files: report.xml
-
- - name: Upload test results to Codecov
- if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }}
- uses: codecov/codecov-action@v6
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- report_type: test_results
-
- name: Archive Maven Local Repository
if: ${{ success() && matrix.os == 'ubuntu-latest' }}
@@ -142,16 +126,16 @@ jobs:
contents: read
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up JDK ${{ env.JAVA_VERSION }}
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v6
+ uses: gradle/actions/setup-gradle@v6.3.0
with:
validate-wrappers: true
add-job-summary: 'always'
@@ -184,16 +168,16 @@ jobs:
name: "Build Sample: ${{ matrix.sample }}"
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up JDK ${{ env.JAVA_VERSION }}
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v6
+ uses: gradle/actions/setup-gradle@v6.3.0
with:
add-job-summary: 'always'
cache-read-only: true
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 5697b61..2e7c58f 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -25,22 +25,22 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Set up JDK ${{ env.JAVA_VERSION }}
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
distribution: ${{ env.JAVA_DISTRIBUTION }}
java-version: ${{ env.JAVA_VERSION }}
- name: Initialize CodeQL
- uses: github/codeql-action/init@v4
+ uses: github/codeql-action/init@v4.38.0
with:
languages: java-kotlin
build-mode: manual
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v6
+ uses: gradle/actions/setup-gradle@v6.3.0
with:
validate-wrappers: true
cache-read-only: true
@@ -52,6 +52,6 @@ jobs:
--no-daemon --rerun-tasks --stacktrace
- name: Analyze
- uses: github/codeql-action/analyze@v4
+ uses: github/codeql-action/analyze@v4.38.0
with:
category: '/language:java-kotlin'
diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml
index 4058d02..b3e3935 100644
--- a/.github/workflows/conformance.yml
+++ b/.github/workflows/conformance.yml
@@ -25,16 +25,16 @@ jobs:
timeout-minutes: 20
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up JDK
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v6
+ uses: gradle/actions/setup-gradle@v6.3.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
@@ -76,16 +76,16 @@ jobs:
- sse-retry
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up JDK
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v6
+ uses: gradle/actions/setup-gradle@v6.3.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
@@ -107,16 +107,16 @@ jobs:
timeout-minutes: 20
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up JDK
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v6
+ uses: gradle/actions/setup-gradle@v6.3.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
@@ -146,16 +146,16 @@ jobs:
- auth/cross-app-access-complete-flow
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up JDK
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v6
+ uses: gradle/actions/setup-gradle@v6.3.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml
index 6073684..e7e94c3 100644
--- a/.github/workflows/dependabot-auto-merge.yml
+++ b/.github/workflows/dependabot-auto-merge.yml
@@ -11,7 +11,7 @@ jobs:
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- - uses: dependabot/fetch-metadata@v2
+ - uses: dependabot/fetch-metadata@v3
id: metadata
- if: >-
diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml
index 6b60e87..f01f9c8 100644
--- a/.github/workflows/gradle-publish.yml
+++ b/.github/workflows/gradle-publish.yml
@@ -17,18 +17,23 @@ jobs:
packages: write
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up JDK 21
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
java-version: '21'
distribution: 'temurin'
- - name: Setup Gradle
- uses: gradle/actions/setup-gradle@v6
+ - name: Set up Node.js
+ uses: actions/setup-node@v7
+ with:
+ node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed
- - name: Build
- run: ./gradlew build --rerun-tasks --max-workers 2 --continue
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v6.3.0
+
+ - name: Check
+ run: ./gradlew check --no-configuration-cache
- name: Publish to Maven Central Portal
id: publish
diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml
index 2c910fd..7463a9e 100644
--- a/.github/workflows/samples.yml
+++ b/.github/workflows/samples.yml
@@ -36,16 +36,16 @@ jobs:
name: Build Sample (${{ matrix.sample }})
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up JDK ${{ env.JAVA_VERSION }}
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v6
+ uses: gradle/actions/setup-gradle@v6.3.0
with:
validate-wrappers: true
add-job-summary: 'always'
diff --git a/.gitignore b/.gitignore
index d9f0fde..805579c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,7 +56,7 @@ node_modules
dist
### SWE agents ###
-.claude/settings.local.json
+.claude/
.junie/
### Conformance test results ###
diff --git a/.idea/detekt.xml b/.idea/detekt.xml
deleted file mode 100644
index d89f908..0000000
--- a/.idea/detekt.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/.idea/externalDependencies.xml b/.idea/externalDependencies.xml
deleted file mode 100644
index 0a081df..0000000
--- a/.idea/externalDependencies.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/.idea/ktlint-plugin.xml b/.idea/ktlint-plugin.xml
deleted file mode 100644
index e8bd90c..0000000
--- a/.idea/ktlint-plugin.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
- DISTRACT_FREE
- DEFAULT
-
-
\ No newline at end of file
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/build.gradle.kts b/build.gradle.kts
index 72c5553..2c2dbe3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,8 +1,5 @@
-import dev.detekt.gradle.extensions.FailOnSeverity
-
plugins {
id("mcp.dokka")
- alias(libs.plugins.detekt)
alias(libs.plugins.ktlint)
alias(libs.plugins.kover)
}
@@ -23,16 +20,6 @@ dependencies {
subprojects {
apply(plugin = "org.jlleitschuh.gradle.ktlint")
apply(plugin = "org.jetbrains.kotlinx.kover")
-
- if (name != "conformance-test" && name != "docs") {
- apply(plugin = "dev.detekt")
-
- detekt {
- config = files("$rootDir/config/detekt/detekt.yml")
- buildUponDefaultConfig = true
- failOnSeverity.set(FailOnSeverity.Error)
- }
- }
}
dokka {
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 84676a4..4a057ce 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -8,6 +8,7 @@ repositories {
}
dependencies {
+ implementation(libs.detekt.gradle)
implementation(libs.kotlin.gradle)
implementation(libs.kotlin.serialization)
implementation(libs.kotlinx.atomicfu.gradle)
diff --git a/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts b/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts
new file mode 100644
index 0000000..3c1419e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/mcp.detekt.gradle.kts
@@ -0,0 +1,23 @@
+import dev.detekt.gradle.Detekt
+import dev.detekt.gradle.extensions.FailOnSeverity
+
+plugins {
+ id("dev.detekt")
+}
+
+detekt {
+ config = files("$rootDir/config/detekt/detekt.yml")
+ buildUponDefaultConfig = false
+ parallel = true
+ failOnSeverity = FailOnSeverity.Error
+}
+
+tasks.withType().configureEach {
+ exclude("**/generated/**", "**/generated-sources/**", "**/build/**")
+}
+
+pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") {
+ tasks.named("detekt").configure {
+ dependsOn("detektMainJvm")
+ }
+}
diff --git a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts
index a331139..98a0a5e 100644
--- a/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts
+++ b/buildSrc/src/main/kotlin/mcp.multiplatform.gradle.kts
@@ -45,11 +45,6 @@ kotlin {
events("failed")
}
systemProperty("kotest.output.ansi", "true")
- reports {
- junitXml.required.set(true)
- junitXml.includeSystemOutLog.set(true)
- junitXml.includeSystemErrLog.set(true)
- }
}
}
macosArm64()
@@ -62,12 +57,3 @@ kotlin {
explicitApi = ExplicitApiMode.Strict
jvmToolchain(21)
}
-
-tasks.named("detekt").configure {
- dependsOn(
- "detektMainJvm",
- "detektCommonMainSourceSet",
- "detektTestJvm",
- "detektCommonTestSourceSet",
- )
-}
diff --git a/codecov.yml b/codecov.yml
deleted file mode 100644
index 9135aae..0000000
--- a/codecov.yml
+++ /dev/null
@@ -1,51 +0,0 @@
-# Codecov configuration for Mokksy and AI-Mocks
-# https://docs.codecov.com/docs/codecov-yaml
-
-codecov:
- require_ci_to_pass: yes
-
-coverage:
- precision: 2
- round: down
- status:
- project:
- default:
- target: auto
- threshold: 1%
- patch:
- default:
- target: auto
- threshold: 1%
-
-comment:
- layout: "header, diff, flags, components"
- behavior: default
- require_changes: true
-
-component_management:
- default_rules:
- statuses:
- - type: project
- target: auto
- individual_components:
-
- - component_id: kotlin-sdk-core
- name: SDK Core
- paths:
- - kotlin-sdk-core/**
-
- - component_id: kotlin-sdk-client
- name: SDK Client
- paths:
- - kotlin-sdk-client/**
-
- - component_id: kotlin-sdk-server
- name: SDK Server
- paths:
- - kotlin-sdk-server/**
-
- - component_id: kotlin-sdk-testing
- name: SDK Testing
- paths:
- - kotlin-sdk-testing/**
-
diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml
index 4d44734..a82b404 100644
--- a/config/detekt/detekt.yml
+++ b/config/detekt/detekt.yml
@@ -3,41 +3,839 @@ config:
warningsAsErrors: false
checkExhaustiveness: false
-complexity:
- LargeClass:
- excludes: &testFolders
- - '**/test/**'
- - '**/commonTest/**'
- - '**/jvmTest/**'
- - '**/jsTest/**'
- - '**/iosTest/**'
- LongMethod:
- excludes: *testFolders
+processors:
+ active: false
-coroutines:
- InjectDispatcher:
- excludes: *testFolders
-
-empty-blocks:
- EmptyFunctionBlock:
- excludes: *testFolders
+console-reports:
+ active: true
+ exclude:
+ - 'ProjectStatisticsReport'
+ - 'ComplexityReport'
+ - 'NotificationReport'
+ - 'IssuesReport'
+ - 'FileBasedIssuesReport'
+ # - 'LiteIssuesReport'
comments:
- UndocumentedPublicClass:
- active: true
- excludes: &testAndGeneratedFolders
- - '**/test/**'
- - '**/commonTest/**'
- - '**/jvmTest/**'
- - '**/jsTest/**'
- - '**/iosTest/**'
- - '**/generated-sources/**'
- ignoreDefaultCompanionObject: true
- UndocumentedPublicFunction:
- active: true
- excludes: *testAndGeneratedFolders
- UndocumentedPublicProperty:
- active: true
- excludes: *testAndGeneratedFolders
- searchProtectedProperty: false
- ignoreEnumEntries: true
+ active: true
+ AbsentOrWrongFileLicense:
+ active: false
+ licenseTemplateIsRegex: false
+ licenseTemplate: ''
+ DeprecatedBlockTag:
+ active: true
+ DocumentationOverPrivateFunction:
+ active: false
+ DocumentationOverPrivateProperty:
+ active: false
+ EndOfSentenceFormat:
+ active: false
+ endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
+ KDocReferencesNonPublicProperty:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ OutdatedDocumentation:
+ active: false
+ matchTypeParameters: true
+ matchDeclarationsOrder: true
+ allowParamOnConstructorProperties: false
+ UndocumentedPublicClass:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ searchInNestedClass: true
+ searchInInnerClass: true
+ searchInInnerObject: true
+ searchInInnerInterface: true
+ searchInProtectedClass: false
+ ignoreDefaultCompanionObject: true
+ UndocumentedPublicFunction:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ searchProtectedFunction: false
+ UndocumentedPublicProperty:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ searchProtectedProperty: false
+ ignoreEnumEntries: false
+
+complexity:
+ active: true
+ CognitiveComplexMethod:
+ active: false
+ allowedComplexity: 20
+ ComplexCondition:
+ active: true
+ allowedConditions: 4
+ ComplexInterface:
+ active: false
+ allowedDefinitions: 10
+ includeStaticDeclarations: false
+ includePrivateDeclarations: false
+ ignoreOverloaded: false
+ CyclomaticComplexMethod:
+ active: false
+ allowedComplexity: 15
+ ignoreSingleWhenExpression: false
+ ignoreSimpleWhenEntries: false
+ ignoreNestingFunctions: false
+ ignoreLocalFunctions: false
+ nestingFunctions:
+ - 'also'
+ - 'apply'
+ - 'forEach'
+ - 'isNotNull'
+ - 'ifNull'
+ - 'let'
+ - 'run'
+ - 'use'
+ - 'with'
+ LabeledExpression:
+ active: false
+ ignoredLabels: []
+ LargeClass:
+ active: false
+ allowedLines: 700
+ LongMethod:
+ active: false
+ allowedLines: 110
+ LongParameterList:
+ active: false
+ allowedFunctionParameters: 8
+ allowedConstructorParameters: 8
+ ignoreDefaultParameters: false
+ ignoreDataClasses: true
+ ignoreAnnotatedParameter: []
+ MethodOverloading:
+ active: false
+ allowedOverloads: 6
+ NamedArguments:
+ active: false
+ allowedArguments: 3
+ ignoreMethods: []
+ ignoreArgumentsMatchingNames: false
+ NestedBlockDepth:
+ active: true
+ allowedDepth: 4
+ NestedScopeFunctions:
+ active: false
+ allowedDepth: 1
+ functions:
+ - 'kotlin.apply'
+ - 'kotlin.run'
+ - 'kotlin.with'
+ - 'kotlin.let'
+ - 'kotlin.also'
+ ReplaceSafeCallChainWithRun:
+ active: false
+ StringLiteralDuplication:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ allowedDuplications: 3
+ ignoreAnnotation: true
+ allowedWithLengthLessThan: 5
+ ignoreStringsRegex: '$^'
+ TooManyFunctions:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ allowedFunctionsPerFile: 20
+ allowedFunctionsPerClass: 20
+ allowedFunctionsPerInterface: 20
+ allowedFunctionsPerObject: 20
+ allowedFunctionsPerEnum: 11
+ ignoreDeprecated: true
+ ignorePrivate: true
+ ignoreInternal: true
+ ignoreOverridden: false
+ ignoreAnnotatedFunctions: []
+
+coroutines:
+ active: true
+ CoroutineLaunchedInTestWithoutRunTest:
+ active: false
+ GlobalCoroutineUsage:
+ active: true
+ InjectDispatcher:
+ active: false
+ dispatcherNames:
+ - 'IO'
+ - 'Default'
+ - 'Unconfined'
+ RedundantSuspendModifier:
+ active: true
+ SleepInsteadOfDelay:
+ active: true
+ SuspendFunInFinallySection:
+ active: false
+ SuspendFunSwallowedCancellation:
+ active: false
+ SuspendFunWithCoroutineScopeReceiver:
+ active: false
+ aliases: ['SuspendFunctionOnCoroutineScope']
+ SuspendFunWithFlowReturnType:
+ active: true
+
+empty-blocks:
+ active: true
+ EmptyCatchBlock:
+ active: true
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ EmptyClassBlock:
+ active: true
+ EmptyDefaultConstructor:
+ active: true
+ EmptyDoWhileBlock:
+ active: true
+ EmptyElseBlock:
+ active: true
+ EmptyFinallyBlock:
+ active: true
+ EmptyForBlock:
+ active: true
+ EmptyFunctionBlock:
+ active: true
+ ignoreOverridden: false
+ EmptyIfBlock:
+ active: true
+ EmptyInitBlock:
+ active: true
+ EmptyKotlinFile:
+ active: true
+ EmptySecondaryConstructor:
+ active: true
+ EmptyTryBlock:
+ active: true
+ EmptyWhenBlock:
+ active: true
+ EmptyWhileBlock:
+ active: true
+
+exceptions:
+ active: true
+ ErrorUsageWithThrowable:
+ active: false
+ ExceptionRaisedInUnexpectedLocation:
+ active: true
+ methodNames:
+ - 'equals'
+ - 'finalize'
+ - 'hashCode'
+ - 'toString'
+ InstanceOfCheckForException:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ NotImplementedDeclaration:
+ active: true
+ ObjectExtendsThrowable:
+ active: true
+ PrintStackTrace:
+ active: true
+ RethrowCaughtException:
+ active: true
+ ReturnFromFinally:
+ active: true
+ ignoreLabeled: false
+ SwallowedException:
+ active: true
+ ignoredExceptionTypes:
+ - 'InterruptedException'
+ - 'MalformedURLException'
+ - 'NumberFormatException'
+ - 'ParseException'
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ ThrowingExceptionFromFinally:
+ active: true
+ ThrowingExceptionInMain:
+ active: false
+ ThrowingExceptionsWithoutMessageOrCause:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ exceptions:
+ - 'ArrayIndexOutOfBoundsException'
+ - 'Exception'
+ - 'IllegalArgumentException'
+ - 'IllegalMonitorStateException'
+ - 'IllegalStateException'
+ - 'IndexOutOfBoundsException'
+ - 'NullPointerException'
+ - 'RuntimeException'
+ - 'Throwable'
+ ThrowingNewInstanceOfSameException:
+ active: true
+ TooGenericExceptionCaught:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ exceptionNames:
+ - 'ArrayIndexOutOfBoundsException'
+ - 'Error'
+ - 'Exception'
+ - 'IllegalMonitorStateException'
+ - 'IndexOutOfBoundsException'
+ - 'NullPointerException'
+ - 'RuntimeException'
+ - 'Throwable'
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ TooGenericExceptionThrown:
+ active: false
+ exceptionNames:
+ - 'Error'
+ - 'Exception'
+ - 'RuntimeException'
+ - 'Throwable'
+
+naming:
+ active: true
+ BooleanPropertyNaming:
+ active: false
+ allowedPattern: '^(is|has|are)'
+ ClassNaming:
+ active: true
+ aliases: ['ClassName']
+ classPattern: '[A-Z][a-zA-Z0-9]*'
+ ConstructorParameterNaming:
+ active: true
+ parameterPattern: '[a-z][A-Za-z0-9]*'
+ privateParameterPattern: '[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ EnumNaming:
+ active: true
+ aliases: ['EnumEntryName']
+ enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
+ ForbiddenClassName:
+ active: false
+ forbiddenName: []
+ FunctionNameMaxLength:
+ active: false
+ aliases: ['FunctionMaxNameLength']
+ maximumFunctionNameLength: 30
+ FunctionNameMinLength:
+ active: false
+ aliases: ['FunctionMinNameLength']
+ minimumFunctionNameLength: 3
+ FunctionNaming:
+ active: true
+ aliases: ['FunctionName']
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ functionPattern: '[a-z][a-zA-Z0-9]*'
+ excludeClassPattern: '$^'
+ FunctionParameterNaming:
+ active: true
+ parameterPattern: '[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ InvalidPackageDeclaration:
+ active: true
+ aliases: ['PackageDirectoryMismatch']
+ rootPackage: ''
+ requireRootInDeclaration: false
+ LambdaParameterNaming:
+ active: false
+ parameterPattern: '[a-z][A-Za-z0-9]*|_'
+ MatchingDeclarationName:
+ active: false
+ mustBeFirst: true
+ multiplatformTargets:
+ - 'ios'
+ - 'android'
+ - 'js'
+ - 'jvm'
+ - 'native'
+ - 'iosArm64'
+ - 'iosX64'
+ - 'macosX64'
+ - 'mingwX64'
+ - 'linuxX64'
+ MemberNameEqualsClassName:
+ active: true
+ ignoreOverridden: true
+ NoNameShadowing:
+ active: true
+ NonBooleanPropertyPrefixedWithIs:
+ active: true
+ ObjectPropertyNaming:
+ active: true
+ aliases: ['ObjectPropertyName']
+ constantPattern: '[A-Za-z][_A-Za-z0-9]*'
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
+ PackageNaming:
+ active: true
+ aliases: ['PackageName']
+ packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
+ TopLevelPropertyNaming:
+ active: true
+ constantPattern: '[A-Z][_A-Z0-9]*'
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
+ VariableMaxLength:
+ active: false
+ maximumVariableNameLength: 64
+ VariableMinLength:
+ active: false
+ minimumVariableNameLength: 1
+ VariableNaming:
+ active: false
+ aliases: ['PropertyName']
+ variablePattern: '[a-z][A-Za-z0-9]*'
+ privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+
+performance:
+ active: true
+ ArrayPrimitive:
+ active: true
+ CouldBeSequence:
+ active: false
+ allowedOperations: 3
+ ForEachOnRange:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ SpreadOperator:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ UnnecessaryInitOnArray:
+ active: false
+ UnnecessaryPartOfBinaryExpression:
+ active: true
+ UnnecessaryTemporaryInstantiation:
+ active: true
+ UnnecessaryTypeCasting:
+ active: true
+
+potential-bugs:
+ active: true
+ AvoidReferentialEquality:
+ active: true
+ forbiddenTypePatterns:
+ - 'kotlin.String'
+ CastNullableToNonNullableType:
+ active: true
+ ignorePlatformTypes: true
+ CastToNullableType:
+ active: false
+ CharArrayToStringCall:
+ active: false
+ Deprecation:
+ active: false
+ aliases: ['DEPRECATION']
+ DontDowncastCollectionTypes:
+ active: false
+ DoubleMutabilityForCollection:
+ active: true
+ aliases: ['DoubleMutability']
+ mutableTypes:
+ - 'kotlin.collections.MutableList'
+ - 'kotlin.collections.MutableMap'
+ - 'kotlin.collections.MutableSet'
+ - 'java.util.ArrayList'
+ - 'java.util.LinkedHashSet'
+ - 'java.util.HashSet'
+ - 'java.util.LinkedHashMap'
+ - 'java.util.HashMap'
+ ElseCaseInsteadOfExhaustiveWhen:
+ active: false
+ ignoredSubjectTypes: []
+ EqualsAlwaysReturnsTrueOrFalse:
+ active: true
+ EqualsWithHashCodeExist:
+ active: true
+ ExitOutsideMain:
+ active: false
+ ExplicitGarbageCollectionCall:
+ active: true
+ HasPlatformType:
+ active: true
+ IgnoredReturnValue:
+ active: true
+ restrictToConfig: true
+ returnValueAnnotations:
+ - 'CheckResult'
+ - '*.CheckResult'
+ - 'CheckReturnValue'
+ - '*.CheckReturnValue'
+ ignoreReturnValueAnnotations:
+ - 'CanIgnoreReturnValue'
+ - '*.CanIgnoreReturnValue'
+ returnValueTypes:
+ - 'kotlin.Function*'
+ - 'kotlin.sequences.Sequence'
+ - 'kotlinx.coroutines.flow.*Flow'
+ - 'java.util.stream.*Stream'
+ ignoreFunctionCall: []
+ ImplicitDefaultLocale:
+ active: true
+ ImplicitUnitReturnType:
+ active: false
+ ignoreAnnotated: ['Test']
+ allowExplicitReturnType: true
+ InvalidRange:
+ active: true
+ IteratorHasNextCallsNextMethod:
+ active: true
+ IteratorNotThrowingNoSuchElementException:
+ active: true
+ LateinitUsage:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ ignoreOnClassesPattern: ''
+ MapGetWithNotNullAssertionOperator:
+ active: true
+ MissingPackageDeclaration:
+ active: true
+ excludes: ['**/*.kts']
+ MissingSuperCall:
+ active: false
+ mustInvokeSuperAnnotations:
+ - 'androidx.annotation.CallSuper'
+ - 'javax.annotation.OverridingMethodsMustInvokeSuper'
+ MissingUseCall:
+ active: false
+ ignoreClass:
+ - 'java.io.ByteArrayInputStream'
+ - 'java.io.ByteArrayOutputStream'
+ NullCheckOnMutableProperty:
+ active: false
+ NullableToStringCall:
+ active: false
+ PropertyUsedBeforeDeclaration:
+ active: false
+ UnconditionalJumpStatementInLoop:
+ active: false
+ UnnamedParameterUse:
+ active: false
+ allowAdjacentDifferentTypeParams: true
+ allowSingleParamUse: true
+ ignoreArgumentsMatchingNames: true
+ ignoreFunctionCall: []
+ UnnecessaryNotNullCheck:
+ active: false
+ UnnecessaryNotNullOperator:
+ active: true
+ UnnecessarySafeCall:
+ active: true
+ UnreachableCatchBlock:
+ active: true
+ UnreachableCode:
+ active: true
+ UnsafeCallOnNullableType:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ UnsafeCast:
+ active: true
+ aliases: ['UNCHECKED_CAST']
+ UnusedUnaryOperator:
+ active: true
+ UselessPostfixExpression:
+ active: true
+ WrongEqualsTypeParameter:
+ active: true
+
+style:
+ active: true
+ AbstractClassCanBeConcreteClass:
+ active: true
+ AbstractClassCanBeInterface:
+ active: true
+ AlsoCouldBeApply:
+ active: false
+ BracesOnIfStatements:
+ active: false
+ singleLine: 'never'
+ multiLine: 'always'
+ BracesOnWhenStatements:
+ active: false
+ singleLine: 'necessary'
+ multiLine: 'consistent'
+ CanBeNonNullable:
+ active: false
+ CascadingCallWrapping:
+ active: false
+ includeElvis: true
+ ClassOrdering:
+ active: false
+ CollapsibleIfStatements:
+ active: false
+ DataClassContainsFunctions:
+ active: false
+ conversionFunctionPrefix:
+ - 'to'
+ allowOperators: false
+ DataClassShouldBeImmutable:
+ active: false
+ DestructuringDeclarationWithTooManyEntries:
+ active: true
+ maxDestructuringEntries: 3
+ DoubleNegativeExpression:
+ active: false
+ DoubleNegativeLambda:
+ active: false
+ negativeFunctions:
+ - reason: 'Use `takeIf` instead.'
+ value: 'takeUnless'
+ - reason: 'Use `all` instead.'
+ value: 'none'
+ negativeFunctionNameParts:
+ - 'not'
+ - 'non'
+ EqualsNullCall:
+ active: true
+ EqualsOnSignatureLine:
+ active: false
+ ExplicitCollectionElementAccessMethod:
+ active: false
+ ExplicitItLambdaMultipleParameters:
+ active: true
+ ExplicitItLambdaParameter:
+ active: false
+ ExpressionBodySyntax:
+ active: false
+ includeLineWrapping: false
+ ForbiddenAnnotation:
+ active: true
+ annotations:
+ - reason: 'it is a java annotation. Use `Suppress` instead.'
+ value: 'java.lang.SuppressWarnings'
+ - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.'
+ value: 'java.lang.Deprecated'
+ - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.'
+ value: 'java.lang.annotation.Documented'
+ - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.'
+ value: 'java.lang.annotation.Target'
+ - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.'
+ value: 'java.lang.annotation.Retention'
+ - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.'
+ value: 'java.lang.annotation.Repeatable'
+ ForbiddenComment:
+ active: true
+ comments:
+ - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
+ value: 'FIXME:'
+ - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
+ value: 'STOPSHIP:'
+ - reason: 'Forbidden TODO todo marker in comment, please do the changes.'
+ value: 'TODO:'
+ allowedPatterns: ''
+ ForbiddenImport:
+ active: false
+ forbiddenImports: []
+ allowedImports: []
+ ForbiddenMethodCall:
+ active: true
+ methods:
+ - reason: 'print does not allow you to configure the output stream. Use a logger instead.'
+ value: 'kotlin.io.print'
+ - reason: 'println does not allow you to configure the output stream. Use a logger instead.'
+ value: 'kotlin.io.println'
+ - reason: 'using `BigDecimal(Double)` can result in unexpected floating point precision behavior. Use `BigDecimal.valueOf(Double)` or `String.toBigDecimalOrNull()` instead.'
+ value: 'java.math.BigDecimal.(kotlin.Double)'
+ - reason: 'using `BigDecimal(String)` can result in a `NumberFormatException`. Use `String.toBigDecimalOrNull()`'
+ value: 'java.math.BigDecimal.(kotlin.String)'
+ - reason: 'It is marked as obsolete. Use `kotlin.time.measureTime` instead.'
+ value: 'kotlin.system.measureTimeMillis'
+ ForbiddenNamedParam:
+ active: false
+ methods: []
+ ForbiddenOptIn:
+ active: false
+ markerClasses: []
+ ForbiddenSuppress:
+ active: false
+ rules: []
+ ForbiddenVoid:
+ active: true
+ ignoreOverridden: false
+ ignoreUsageInGenerics: false
+ FunctionOnlyReturningConstant:
+ active: true
+ ignoreOverridableFunction: true
+ ignoreActualFunction: true
+ excludedFunctions: []
+ LoopWithTooManyJumpStatements:
+ active: true
+ maxJumpCount: 1
+ MagicNumber:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts']
+ ignoreNumbers:
+ - '-1'
+ - '0'
+ - '1'
+ - '2'
+ ignoreHashCodeFunction: true
+ ignorePropertyDeclaration: true
+ ignoreLocalVariableDeclaration: true
+ ignoreConstantDeclaration: true
+ ignoreCompanionObjectPropertyDeclaration: true
+ ignoreAnnotation: false
+ ignoreNamedArgument: true
+ ignoreEnums: false
+ ignoreRanges: false
+ ignoreExtensionFunctions: true
+ MandatoryBracesLoops:
+ active: false
+ MaxChainedCallsOnSameLine:
+ active: false
+ maxChainedCalls: 5
+ MaxLineLength:
+ active: false
+ maxLineLength: 120
+ excludePackageStatements: true
+ excludeImportStatements: true
+ excludeCommentStatements: false
+ excludeRawStrings: true
+ MayBeConstant:
+ active: true
+ ModifierOrder:
+ active: true
+ MultilineLambdaItParameter:
+ active: false
+ MultilineRawStringIndentation:
+ active: false
+ indentSize: 4
+ trimmingMethods:
+ - 'trimIndent'
+ - 'trimMargin'
+ NestedClassesVisibility:
+ active: true
+ NewLineAtEndOfFile:
+ active: true
+ NoTabs:
+ active: false
+ NullableBooleanCheck:
+ active: false
+ ObjectLiteralToLambda:
+ active: true
+ OptionalAbstractKeyword:
+ active: true
+ OptionalUnit:
+ active: false
+ ProtectedMemberInFinalClass:
+ active: true
+ RangeUntilInsteadOfRangeTo:
+ active: true
+ RedundantConstructorKeyword:
+ active: false
+ RedundantExplicitType:
+ active: false
+ RedundantHigherOrderMapUsage:
+ active: true
+ RedundantVisibilityModifier:
+ active: false
+ ReturnCount:
+ active: false
+ max: 2
+ excludedFunctions:
+ - 'equals'
+ excludeLabeled: false
+ excludeReturnFromLambda: true
+ excludeGuardClauses: false
+ SafeCast:
+ active: true
+ SerialVersionUIDInSerializableClass:
+ active: true
+ SpacingAfterPackageAndImports:
+ active: false
+ StringShouldBeRawString:
+ active: false
+ maxEscapedCharacterCount: 2
+ ignoredCharacters: []
+ ThrowsCount:
+ active: false
+ max: 2
+ excludeGuardClauses: false
+ TrailingWhitespace:
+ active: false
+ TrimMultilineRawString:
+ active: false
+ trimmingMethods:
+ - 'trimIndent'
+ - 'trimMargin'
+ UnderscoresInNumericLiterals:
+ active: false
+ acceptableLength: 4
+ allowNonStandardGrouping: false
+ UnnecessaryAny:
+ active: false
+ UnnecessaryApply:
+ active: true
+ UnnecessaryBackticks:
+ active: false
+ UnnecessaryBracesAroundTrailingLambda:
+ active: false
+ UnnecessaryFilter:
+ active: true
+ UnnecessaryFullyQualifiedName:
+ active: true
+ UnnecessaryInheritance:
+ active: true
+ UnnecessaryInnerClass:
+ active: false
+ UnnecessaryLet:
+ active: false
+ UnnecessaryParentheses:
+ active: false
+ allowForUnclearPrecedence: false
+ UnnecessaryReversed:
+ active: false
+ UnusedImport:
+ active: false
+ additionalOperatorSet: []
+ UnusedParameter:
+ active: true
+ aliases: ['UNUSED_PARAMETER', 'unused']
+ allowedNames: 'ignored|expected'
+ UnusedPrivateClass:
+ active: true
+ aliases: ['unused']
+ UnusedPrivateFunction:
+ active: true
+ aliases: ['unused']
+ allowedNames: ''
+ UnusedPrivateProperty:
+ active: true
+ aliases: ['unused']
+ allowedNames: 'ignored|expected|serialVersionUID'
+ UnusedVariable:
+ active: true
+ aliases: ['UNUSED_VARIABLE', 'unused']
+ allowedNames: 'ignored|_'
+ UseAnyOrNoneInsteadOfFind:
+ active: true
+ UseArrayLiteralsInAnnotations:
+ active: true
+ UseCheckNotNull:
+ active: true
+ UseCheckOrError:
+ active: true
+ UseDataClass:
+ active: false
+ allowVars: false
+ UseEmptyCounterpart:
+ active: false
+ UseIfEmptyOrIfBlank:
+ active: false
+ UseIfInsteadOfWhen:
+ active: false
+ ignoreWhenContainingVariableDeclaration: false
+ UseIsNullOrEmpty:
+ active: true
+ UseLet:
+ active: false
+ UseOrEmpty:
+ active: true
+ UseRequire:
+ active: true
+ UseRequireNotNull:
+ active: true
+ UseSumOfInsteadOfFlatMapSize:
+ active: false
+ UselessCallOnNotNull:
+ active: true
+ UtilityClassWithPublicConstructor:
+ active: true
+ VarCouldBeVal:
+ active: true
+ aliases: ['CanBeVal']
+ ignoreLateinitVar: false
+ WildcardImport:
+ active: false
+ excludeImports:
+ - 'java.util.*'
diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt
index 03d5ee3..8a18957 100644
--- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt
+++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceServer.kt
@@ -12,8 +12,6 @@ fun main() {
val port = System.getenv("MCP_PORT")?.toIntOrNull() ?: 3001
embeddedServer(CIO, port = port) {
mcpStreamableHttp(
- enableDnsRebindingProtection = true,
- allowedHosts = listOf("localhost", "127.0.0.1", "localhost:$port", "127.0.0.1:$port"),
eventStore = InMemoryEventStore(),
) {
createConformanceServer()
diff --git a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt
index 54d5c8c..c81c98c 100644
--- a/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt
+++ b/conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt
@@ -45,7 +45,6 @@ internal const val WAV_BASE64 = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABA
private val logger = KotlinLogging.logger {}
-@Suppress("LongMethod")
fun Server.registerConformanceTools() {
// 1. Simple text
addTool(
@@ -176,11 +175,17 @@ fun Server.registerConformanceTools() {
CreateMessageRequest(
CreateMessageRequestParams(
maxTokens = 10000,
- messages = listOf(SamplingMessage(Role.User, TextContent(prompt))),
+ messages = listOf(SamplingMessage(Role.User, listOf(TextContent(prompt)))),
),
),
)
- CallToolResult(listOf(TextContent(result.content.toString())))
+ val sampledText = result.content.joinToString("\n") { content ->
+ when (content) {
+ is TextContent -> content.text
+ else -> "Non-text sampling content: ${content::class.simpleName}"
+ }
+ }
+ CallToolResult(listOf(TextContent(sampledText)))
}
// 9. Elicitation
diff --git a/context7.json b/context7.json
index a28c0fc..5fde250 100644
--- a/context7.json
+++ b/context7.json
@@ -12,7 +12,6 @@
"CONTRIBUTING.md",
"LICENSE",
"SECURITY.md",
- "codecov.yml",
"gradlew",
"gradlew.bat",
".editorconfig",
diff --git a/gradle.properties b/gradle.properties
index ca4a217..a5655c3 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -8,4 +8,4 @@ kotlin.daemon.jvmargs=-Xmx4G
kotlin.native.ignoreDisabledTargets=true
group=io.modelcontextprotocol
-version=0.11.1
+version=0.15.0
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f2836ef..a8d034f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,34 +1,33 @@
[versions]
# plugins version
-atomicfu = "0.32.1"
-binaryCompatibilityValidatorPlugin = "0.18.1"
-detekt = "2.0.0-alpha.2"
+atomicfu = "0.33.0"
+binaryCompatibilityValidatorPlugin = "0.18.2"
+detekt = "2.0.0-alpha.6"
dokka = "2.2.0"
-kotlin = "2.3.10"
-kover = "0.9.8"
+kotlin = "2.4.10"
+kover = "0.9.9"
ktlint = "14.2.0"
knit = "0.5.1"
-mavenPublish = "0.36.0"
-netty = "4.2.12.Final"
-openapi-generator = "7.21.0"
+mavenPublish = "0.37.0"
+netty = "4.2.18.Final"
# libraries version
awaitility = "4.3.0"
-collections-immutable = "0.4.0"
-coroutines = "1.10.2"
-kotest = "6.1.11"
-kotlinx-io = "0.9.0"
-ktor = "3.3.3"
-logging = "8.0.01"
-mockk = "1.14.9"
-mokksy = "0.8.1"
-serialization = "1.10.0"
-slf4j = "2.0.17"
-logback = "1.5.32"
-junit = "5.14.3"
+collections-immutable = "0.5.2"
+coroutines = "1.11.0"
+kotest = "6.2.5"
+kotlinx-io = "0.9.1"
+ktor = "3.5.2"
+logging = "8.0.4"
+mockk = "1.14.11"
+serialization = "1.11.0"
+slf4j = "2.0.19"
+logback = "1.6.3"
+junit = "5.14.4"
[libraries]
# Plugins
+detekt-gradle = { module = "dev.detekt:detekt-gradle-plugin", version.ref = "detekt" }
dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
@@ -65,7 +64,6 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor
ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" }
ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
-mokksy = { group = "dev.mokksy", name = "mokksy", version.ref = "mokksy" }
netty-bom = { group = "io.netty", name = "netty-bom", version.ref = "netty" }
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
@@ -76,9 +74,7 @@ ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "
ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" }
[plugins]
-detekt = { id = "dev.detekt", version.ref = "detekt" }
kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidatorPlugin" }
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
knit = { id = "org.jetbrains.kotlinx.knit", version.ref = "knit" }
-openapi-generator = { id = "org.openapi.generator", version.ref = "openapi-generator" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index d997cfc..eddabd2 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index c61a118..ad7845b 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
+retries=0
+retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 739907d..249efbb 100755
--- a/gradlew
+++ b/gradlew
@@ -20,7 +20,7 @@
##############################################################################
#
-# Gradle start up script for POSIX generated by Gradle.
+# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
-# ksh Gradle
+# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
diff --git a/gradlew.bat b/gradlew.bat
index c4bdd3a..8508ef6 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,93 +1,82 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-@rem SPDX-License-Identifier: Apache-2.0
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@rem This is normally unused
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if %ERRORLEVEL% equ 0 goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem gradlew startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute gradlew
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
+
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/integration-test/build.gradle.kts b/integration-test/build.gradle.kts
index 30afe9e..7a3a4a3 100644
--- a/integration-test/build.gradle.kts
+++ b/integration-test/build.gradle.kts
@@ -1,5 +1,6 @@
plugins {
id("mcp.multiplatform")
+ id("mcp.detekt")
}
kotlin {
diff --git a/integration-test/detekt-baseline-test.xml b/integration-test/detekt-baseline-test.xml
deleted file mode 100644
index 2df611a..0000000
--- a/integration-test/detekt-baseline-test.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
- AbstractClassCanBeConcreteClass:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$AbstractPromptIntegrationTest
- AbstractClassCanBeConcreteClass:AbstractResourceIntegrationTest.kt:AbstractResourceIntegrationTest$AbstractResourceIntegrationTest
- AbstractClassCanBeConcreteClass:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$AbstractToolIntegrationTest
- AbstractClassCanBeConcreteClass:BaseTransportTest.kt:BaseTransportTest$BaseTransportTest
- CyclomaticComplexMethod:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$private fun setupCalculatorTool
- ForbiddenComment:StdioClientTransportTest.kt:StdioClientTransportTest$// TODO: fix running on windows
- MatchingDeclarationName:PromptIntegrationTestSse.kt:SchemaPromptIntegrationTestSse : AbstractPromptIntegrationTest
- ThrowsCount:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$override fun configureServer
- UseCheckOrError:ClientTest.kt:ClientTest.<no name provided>$throw IllegalStateException("Test error")
- UseOrEmpty:AbstractToolIntegrationTest.kt:AbstractToolIntegrationTest$(request.params.arguments?.get("tags") as? JsonArray)?.mapNotNull { (it as? JsonPrimitive)?.content } ?: emptyList()
- UseRequire:AbstractPromptIntegrationTest.kt:AbstractPromptIntegrationTest$throw IllegalArgumentException("Missing required argument: $argName")
-
-
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 d0acda5..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
@@ -14,13 +14,17 @@ import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult
import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams
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.ElicitationCompleteNotificationParams
import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
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
@@ -46,8 +50,10 @@ import io.modelcontextprotocol.kotlin.sdk.types.Tool
import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema
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
@@ -67,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
@@ -106,7 +113,7 @@ class ClientTest {
),
options = ClientOptions(
capabilities = ClientCapabilities(
- sampling = EmptyJsonObject,
+ sampling = ClientCapabilities.sampling,
),
),
)
@@ -152,7 +159,7 @@ class ClientTest {
),
options = ClientOptions(
capabilities = ClientCapabilities(
- sampling = EmptyJsonObject,
+ sampling = ClientCapabilities.sampling,
),
),
)
@@ -339,7 +346,7 @@ class ClientTest {
val client = Client(
clientInfo = Implementation(name = "test client", version = "1.0"),
options = ClientOptions(
- capabilities = ClientCapabilities(sampling = EmptyJsonObject),
+ capabilities = ClientCapabilities(sampling = ClientCapabilities.sampling),
),
)
@@ -539,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)
@@ -564,6 +580,7 @@ class ClientTest {
def.await()
runCatching { job.cancel("Cancelled by test") }
defCancel.await()
+ cancelledOnWire.await()
defTimeOut.await()
}
@@ -602,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
@@ -633,18 +642,16 @@ class ClientTest {
),
options = ClientOptions(
capabilities = ClientCapabilities(
- sampling = EmptyJsonObject,
+ sampling = ClientCapabilities.sampling,
),
),
)
client.setRequestHandler(Method.Defined.SamplingCreateMessage) { _, _ ->
CreateMessageResult(
- model = "test-model",
role = Role.Assistant,
- content = TextContent(
- text = "Test response",
- ),
+ content = TextContent(text = "Test response"),
+ model = "test-model",
)
}
@@ -670,7 +677,7 @@ class ClientTest {
val client = Client(
clientInfo = Implementation(name = "test client", version = "1.0"),
options = ClientOptions(
- capabilities = ClientCapabilities(sampling = EmptyJsonObject),
+ capabilities = ClientCapabilities(sampling = ClientCapabilities.sampling),
),
)
@@ -893,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()
@@ -913,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
@@ -1060,7 +1064,7 @@ class ClientTest {
Implementation(name = "test client", version = "1.0"),
ClientOptions(
capabilities = ClientCapabilities(
- elicitation = EmptyJsonObject,
+ elicitation = ClientCapabilities.Elicitation(),
),
),
)
@@ -1120,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()
@@ -1253,6 +1354,189 @@ class ClientTest {
client.close()
}
+ // ── URL-mode elicitation (SEP-1036) ─────────────────────────────────
+
+ @Test
+ fun `should handle URL mode elicitation end-to-end`() = runTest {
+ val client = Client(
+ Implementation(name = "test client", version = "1.0"),
+ ClientOptions(
+ capabilities = ClientCapabilities(
+ elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject),
+ ),
+ ),
+ )
+
+ val elicitationId = "550e8400-e29b-41d4-a716-446655440000"
+ val url = "https://oauth.example.com/authorize"
+
+ client.setElicitationHandler { request ->
+ val params = assertIs(request.params)
+ assertEquals(elicitationId, params.elicitationId)
+ assertEquals(url, params.url)
+ ElicitResult(action = ElicitResult.Action.Accept)
+ }
+
+ val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
+ val server = Server(
+ serverInfo = Implementation(name = "test server", version = "1.0"),
+ options = ServerOptions(capabilities = ServerCapabilities()),
+ )
+
+ val serverSessionResult = CompletableDeferred()
+ listOf(
+ launch { client.connect(clientTransport) },
+ launch { serverSessionResult.complete(server.createSession(serverTransport)) },
+ ).joinAll()
+ val serverSession = serverSessionResult.await()
+
+ val result = serverSession.createElicitation(
+ message = "Authorize access to continue",
+ elicitationId = elicitationId,
+ url = url,
+ )
+
+ assertEquals(ElicitResult.Action.Accept, result.action)
+ assertNull(result.content)
+
+ client.close()
+ }
+
+ @Test
+ fun `should reject URL mode elicitation when client supports only form mode`() = runTest {
+ val (client, serverSession) = setupElicitationPair {
+ ElicitResult(action = ElicitResult.Action.Accept)
+ }
+
+ val exception = assertFailsWith {
+ serverSession.createElicitation(
+ message = "Authorize",
+ elicitationId = "id-1",
+ url = "https://example.com/auth",
+ )
+ }
+ assertTrue(exception.message!!.contains("elicitation.url"))
+
+ client.close()
+ }
+
+ @Test
+ fun `should deliver elicitation complete notification to client`() = runTest {
+ val received = CompletableDeferred()
+ val client = Client(
+ Implementation(name = "test client", version = "1.0"),
+ ClientOptions(
+ capabilities = ClientCapabilities(
+ elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject),
+ ),
+ ),
+ )
+ client.setElicitationCompleteHandler { received.complete(it) }
+
+ val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
+ val server = Server(
+ serverInfo = Implementation(name = "test server", version = "1.0"),
+ options = ServerOptions(capabilities = ServerCapabilities()),
+ )
+
+ val serverSessionResult = CompletableDeferred()
+ listOf(
+ launch { client.connect(clientTransport) },
+ launch { serverSessionResult.complete(server.createSession(serverTransport)) },
+ ).joinAll()
+ val serverSession = serverSessionResult.await()
+
+ val elicitationId = "complete-id-1"
+ serverSession.sendElicitationComplete(
+ ElicitationCompleteNotification(ElicitationCompleteNotificationParams(elicitationId = elicitationId)),
+ )
+
+ val notification = received.await()
+ assertEquals(elicitationId, notification.params.elicitationId)
+
+ client.close()
+ }
+
+ @Test
+ fun `should reject elicitation complete when client supports only form mode`() = runTest {
+ val (client, serverSession) = setupElicitationPair {
+ ElicitResult(action = ElicitResult.Action.Accept)
+ }
+
+ val exception = assertFailsWith {
+ serverSession.sendElicitationComplete(
+ ElicitationCompleteNotification(ElicitationCompleteNotificationParams(elicitationId = "id-1")),
+ )
+ }
+ assertTrue(exception.message!!.contains("elicitation.url"))
+
+ client.close()
+ }
+
+ @Test
+ fun `setElicitationCompleteHandler should require url capability`() = runTest {
+ val client = Client(
+ Implementation(name = "test client", version = "1.0"),
+ ClientOptions(
+ capabilities = ClientCapabilities(
+ elicitation = ClientCapabilities.Elicitation(),
+ ),
+ ),
+ )
+
+ assertFailsWith {
+ client.setElicitationCompleteHandler { }
+ }
+ }
+
+ @Test
+ fun `should surface URL elicitation required error to client as typed exception`() = runTest {
+ val client = Client(
+ Implementation(name = "test client", version = "1.0"),
+ ClientOptions(
+ capabilities = ClientCapabilities(
+ elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject),
+ ),
+ ),
+ )
+
+ val elicitationId = "auth-required-1"
+ val url = "https://oauth.example.com/authorize"
+
+ val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
+ val server = Server(
+ serverInfo = Implementation(name = "test server", version = "1.0"),
+ options = ServerOptions(capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(true))),
+ )
+ server.addTool("needs-auth", "Requires URL elicitation") {
+ throw UrlElicitationRequiredException(
+ listOf(
+ ElicitRequestURLParams(
+ message = "Authorize to continue",
+ elicitationId = elicitationId,
+ url = url,
+ ),
+ ),
+ )
+ }
+
+ val serverSessionResult = CompletableDeferred()
+ listOf(
+ launch { client.connect(clientTransport) },
+ launch { serverSessionResult.complete(server.createSession(serverTransport)) },
+ ).joinAll()
+ serverSessionResult.await()
+
+ val exception = assertFailsWith {
+ client.callTool(name = "needs-auth", arguments = emptyMap())
+ }
+ val elicitation = exception.elicitations.single()
+ assertEquals(elicitationId, elicitation.elicitationId)
+ assertEquals(url, elicitation.url)
+
+ client.close()
+ }
+
private fun defaultsTestSchema(): ElicitRequestParams.RequestedSchema = ElicitRequestParams.RequestedSchema(
properties = mapOf(
"name" to StringSchema(description = "User name", default = "John Doe"),
@@ -1272,7 +1556,7 @@ class ClientTest {
): Pair = kotlinx.coroutines.coroutineScope {
val client = Client(
Implementation(name = "test client", version = "1.0"),
- ClientOptions(capabilities = ClientCapabilities(elicitation = EmptyJsonObject)),
+ ClientOptions(capabilities = ClientCapabilities(elicitation = ClientCapabilities.Elicitation())),
)
client.setElicitationHandler(handler)
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt
index 585af3f..fc3c1d3 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt
@@ -7,9 +7,13 @@ import io.modelcontextprotocol.kotlin.sdk.types.McpException
import io.modelcontextprotocol.kotlin.test.utils.createSleepyProcessBuilder
import io.modelcontextprotocol.kotlin.test.utils.createTeeProcessBuilder
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
+import kotlinx.coroutines.withTimeoutOrNull
+import kotlinx.io.Buffer
+import kotlinx.io.RawSource
import kotlinx.io.asSink
import kotlinx.io.asSource
import kotlinx.io.buffered
@@ -18,10 +22,12 @@ import org.junit.jupiter.api.Timeout
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.condition.DisabledOnOs
import org.junit.jupiter.api.condition.OS
+import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.fail
import kotlin.time.Duration.Companion.seconds
@@ -109,6 +115,43 @@ class StdioClientTransportTest : BaseTransportTest() {
}
}
+ @OptIn(ExperimentalAtomicApi::class)
+ @Test
+ fun `should close cleanly while stdin read is blocked`(): Unit = runBlocking(Dispatchers.IO) {
+ val input = BlockingRawSource()
+
+ val transport = StdioClientTransport(
+ input = input.buffered(),
+ output = Buffer(),
+ )
+
+ val didClose = AtomicBoolean(false)
+ transport.onClose { didClose.store(true) }
+ transport.onError { error ->
+ fail("Unexpected error while closing transport: $error")
+ }
+
+ transport.start()
+
+ eventually(2.seconds) {
+ assertTrue(input.readStarted, "Transport should start reading stdin before close")
+ }
+
+ val closeJob = async {
+ transport.close()
+ }
+
+ val closed = withTimeoutOrNull(1.seconds) {
+ closeJob.await()
+ }
+
+ input.close()
+ closeJob.await()
+
+ assertNotNull(closed, "Transport.close() should not wait for stdin to produce data or EOF")
+ assertTrue(didClose.load(), "Transport should be closed after close() call")
+ }
+
@Test
fun `should read messages`() = runTest {
val processBuilder = createTeeProcessBuilder()
@@ -147,4 +190,22 @@ class StdioClientTransportTest : BaseTransportTest() {
process.waitFor()
process.destroyForcibly()
}
+
+ private class BlockingRawSource : RawSource {
+ private val closed = CountDownLatch(1)
+
+ @Volatile
+ var readStarted: Boolean = false
+ private set
+
+ override fun readAtMostTo(sink: Buffer, byteCount: Long): Long {
+ readStarted = true
+ closed.await()
+ return -1L
+ }
+
+ override fun close() {
+ closed.countDown()
+ }
+ }
}
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/integration/kotlin/AbstractPromptIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt
index d14ae88..fb97782 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractPromptIntegrationTest.kt
@@ -6,9 +6,15 @@ 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.ListPromptsRequest
+import io.modelcontextprotocol.kotlin.sdk.types.ListPromptsResult
import io.modelcontextprotocol.kotlin.sdk.types.McpException
+import io.modelcontextprotocol.kotlin.sdk.types.Method
+import io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequestParams
+import io.modelcontextprotocol.kotlin.sdk.types.Prompt
import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument
import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage
+import io.modelcontextprotocol.kotlin.sdk.types.RPCError
import io.modelcontextprotocol.kotlin.sdk.types.Role
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
@@ -18,7 +24,9 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
+import java.util.concurrent.CopyOnWriteArrayList
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@@ -690,11 +698,76 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() {
val expectedMessage = "Prompt not found: non-existent-prompt"
- withClue("Exception code should be INTERNAL_ERROR: -32603") {
- exception.code shouldBe -32603
+ withClue("Exception code should be INVALID_PARAMS: ${RPCError.ErrorCode.INVALID_PARAMS}") {
+ exception.code shouldBe RPCError.ErrorCode.INVALID_PARAMS
}
withClue("Unexpected error message for non-existent prompt") {
exception.message shouldBe expectedMessage
}
}
+
+ @Test
+ fun testListPromptsPagination() = runBlocking(Dispatchers.IO) {
+ val receivedCursors = CopyOnWriteArrayList()
+ val page1 = listOf(Prompt(name = "p-1", description = "desc"), Prompt(name = "p-2", description = "desc"))
+ val page2 = listOf(Prompt(name = "p-3", description = "desc"), Prompt(name = "p-4", description = "desc"))
+ val page3 = listOf(Prompt(name = "p-5", description = "desc"))
+
+ server.sessions.forEach { (_, session) ->
+ session.setRequestHandler(Method.Defined.PromptsList) { request, _ ->
+ receivedCursors += request.cursor
+ when (request.cursor) {
+ null -> ListPromptsResult(prompts = page1, nextCursor = "cursor-2")
+ "cursor-2" -> ListPromptsResult(prompts = page2, nextCursor = "cursor-3")
+ "cursor-3" -> ListPromptsResult(prompts = page3, nextCursor = null)
+ else -> error("Unexpected cursor: ${request.cursor}")
+ }
+ }
+ }
+
+ val collected = mutableListOf()
+ var cursor: String? = null
+ do {
+ val request = if (cursor == null) {
+ ListPromptsRequest()
+ } else {
+ ListPromptsRequest(PaginatedRequestParams(cursor = cursor))
+ }
+ val response = client.listPrompts(request)
+ collected += response.prompts
+ cursor = response.nextCursor
+ } while (cursor != null)
+
+ assertEquals(
+ listOf(null, "cursor-2", "cursor-3"),
+ receivedCursors.toList(),
+ "Client must forward each nextCursor into the next request",
+ )
+ assertEquals(
+ listOf("p-1", "p-2", "p-3", "p-4", "p-5"),
+ collected.map { it.name },
+ "Client must accumulate pages in order without duplicates or reordering",
+ )
+ }
+
+ @Test
+ fun testListPromptsInvalidCursor() = runBlocking(Dispatchers.IO) {
+ val invalidCursor = "not-a-valid-cursor"
+
+ server.sessions.forEach { (_, session) ->
+ session.setRequestHandler(Method.Defined.PromptsList) { _, _ ->
+ throw McpException(
+ code = RPCError.ErrorCode.INVALID_PARAMS,
+ message = "Invalid cursor: $invalidCursor",
+ )
+ }
+ }
+
+ val exception = assertFailsWith {
+ client.listPrompts(ListPromptsRequest(PaginatedRequestParams(cursor = invalidCursor)))
+ }
+
+ assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code)
+ assertEquals("Invalid cursor: $invalidCursor", exception.message)
+ }
}
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 4fc53a5..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
@@ -1,11 +1,16 @@
package io.modelcontextprotocol.kotlin.sdk.integration.kotlin
import io.modelcontextprotocol.kotlin.sdk.types.BlobResourceContents
+import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesRequest
+import io.modelcontextprotocol.kotlin.sdk.types.ListResourcesResult
import io.modelcontextprotocol.kotlin.sdk.types.McpException
+import io.modelcontextprotocol.kotlin.sdk.types.Method
+import io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequestParams
import io.modelcontextprotocol.kotlin.sdk.types.RPCError
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.Resource
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequest
import io.modelcontextprotocol.kotlin.sdk.types.SubscribeRequestParams
@@ -18,8 +23,10 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
+import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@@ -70,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,
@@ -309,4 +299,76 @@ abstract class AbstractResourceIntegrationTest : KotlinTestBase() {
assertTrue(result.contents.isNotEmpty(), "Result contents should not be empty")
}
}
+
+ @Test
+ fun testListResourcesPagination() = runBlocking(Dispatchers.IO) {
+ val receivedCursors = CopyOnWriteArrayList()
+ val page1 = listOf(
+ Resource(uri = "test://r-1", name = "r-1"),
+ Resource(uri = "test://r-2", name = "r-2"),
+ Resource(uri = "test://r-3", name = "r-3"),
+ )
+ val page2 = listOf(
+ Resource(uri = "test://r-4", name = "r-4"),
+ Resource(uri = "test://r-5", name = "r-5"),
+ )
+ val page3 = listOf(Resource(uri = "test://r-6", name = "r-6"))
+
+ server.sessions.forEach { (_, session) ->
+ session.setRequestHandler(Method.Defined.ResourcesList) { request, _ ->
+ receivedCursors += request.cursor
+ when (request.cursor) {
+ null -> ListResourcesResult(resources = page1, nextCursor = "cursor-2")
+ "cursor-2" -> ListResourcesResult(resources = page2, nextCursor = "cursor-3")
+ "cursor-3" -> ListResourcesResult(resources = page3, nextCursor = null)
+ else -> error("Unexpected cursor: ${request.cursor}")
+ }
+ }
+ }
+
+ val collected = mutableListOf()
+ var cursor: String? = null
+ do {
+ val request = if (cursor == null) {
+ ListResourcesRequest()
+ } else {
+ ListResourcesRequest(PaginatedRequestParams(cursor = cursor))
+ }
+ val response = client.listResources(request)
+ collected += response.resources
+ cursor = response.nextCursor
+ } while (cursor != null)
+
+ assertEquals(
+ listOf(null, "cursor-2", "cursor-3"),
+ receivedCursors.toList(),
+ "Client must forward each nextCursor into the next request",
+ )
+ assertEquals(
+ listOf("test://r-1", "test://r-2", "test://r-3", "test://r-4", "test://r-5", "test://r-6"),
+ collected.map { it.uri },
+ "Client must accumulate pages in order without duplicates or reordering",
+ )
+ }
+
+ @Test
+ fun testListResourcesInvalidCursor() = runBlocking(Dispatchers.IO) {
+ val invalidCursor = "not-a-valid-cursor"
+
+ server.sessions.forEach { (_, session) ->
+ session.setRequestHandler(Method.Defined.ResourcesList) { _, _ ->
+ throw McpException(
+ code = RPCError.ErrorCode.INVALID_PARAMS,
+ message = "Invalid cursor: $invalidCursor",
+ )
+ }
+ }
+
+ val exception = assertFailsWith {
+ client.listResources(ListResourcesRequest(PaginatedRequestParams(cursor = invalidCursor)))
+ }
+
+ assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code)
+ assertEquals("Invalid cursor: $invalidCursor", exception.message)
+ }
}
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt
index 7da82cc..b802122 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/AbstractToolIntegrationTest.kt
@@ -6,8 +6,15 @@ import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams
import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
import io.modelcontextprotocol.kotlin.sdk.types.ContentBlock
import io.modelcontextprotocol.kotlin.sdk.types.ImageContent
+import io.modelcontextprotocol.kotlin.sdk.types.ListToolsRequest
+import io.modelcontextprotocol.kotlin.sdk.types.ListToolsResult
+import io.modelcontextprotocol.kotlin.sdk.types.McpException
+import io.modelcontextprotocol.kotlin.sdk.types.Method
+import io.modelcontextprotocol.kotlin.sdk.types.PaginatedRequestParams
+import io.modelcontextprotocol.kotlin.sdk.types.RPCError
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
+import io.modelcontextprotocol.kotlin.sdk.types.Tool
import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -24,7 +31,9 @@ import org.junit.jupiter.api.Test
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Locale
+import java.util.concurrent.CopyOnWriteArrayList
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@@ -791,4 +800,75 @@ abstract class AbstractToolIntegrationTest : KotlinTestBase() {
"Error message should indicate the tool was not found",
)
}
+
+ @Test
+ fun testListToolsPagination() = runBlocking(Dispatchers.IO) {
+ val receivedCursors = CopyOnWriteArrayList()
+ val page1 = listOf(
+ Tool(name = "t-1", inputSchema = ToolSchema()),
+ Tool(name = "t-2", inputSchema = ToolSchema()),
+ )
+ val page2 = listOf(
+ Tool(name = "t-3", inputSchema = ToolSchema()),
+ Tool(name = "t-4", inputSchema = ToolSchema()),
+ )
+ val page3 = listOf(Tool(name = "t-5", inputSchema = ToolSchema()))
+
+ server.sessions.forEach { (_, session) ->
+ session.setRequestHandler(Method.Defined.ToolsList) { request, _ ->
+ receivedCursors += request.cursor
+ when (request.cursor) {
+ null -> ListToolsResult(tools = page1, nextCursor = "cursor-2")
+ "cursor-2" -> ListToolsResult(tools = page2, nextCursor = "cursor-3")
+ "cursor-3" -> ListToolsResult(tools = page3, nextCursor = null)
+ else -> error("Unexpected cursor: ${request.cursor}")
+ }
+ }
+ }
+
+ val collected = mutableListOf()
+ var cursor: String? = null
+ do {
+ val request = if (cursor == null) {
+ ListToolsRequest()
+ } else {
+ ListToolsRequest(PaginatedRequestParams(cursor = cursor))
+ }
+ val response = client.listTools(request)
+ collected += response.tools
+ cursor = response.nextCursor
+ } while (cursor != null)
+
+ assertEquals(
+ listOf(null, "cursor-2", "cursor-3"),
+ receivedCursors.toList(),
+ "Client must forward each nextCursor into the next request",
+ )
+ assertEquals(
+ listOf("t-1", "t-2", "t-3", "t-4", "t-5"),
+ collected.map { it.name },
+ "Client must accumulate pages in order without duplicates or reordering",
+ )
+ }
+
+ @Test
+ fun testListToolsInvalidCursor() = runBlocking(Dispatchers.IO) {
+ val invalidCursor = "not-a-valid-cursor"
+
+ server.sessions.forEach { (_, session) ->
+ session.setRequestHandler(Method.Defined.ToolsList) { _, _ ->
+ throw McpException(
+ code = RPCError.ErrorCode.INVALID_PARAMS,
+ message = "Invalid cursor: $invalidCursor",
+ )
+ }
+ }
+
+ val exception = assertFailsWith {
+ client.listTools(ListToolsRequest(PaginatedRequestParams(cursor = invalidCursor)))
+ }
+
+ assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code)
+ assertEquals("Invalid cursor: $invalidCursor", exception.message)
+ }
}
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/StreamableHttpClientTransportIntegrationTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpClientTransportIntegrationTest.kt
new file mode 100644
index 0000000..598825f
--- /dev/null
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpClientTransportIntegrationTest.kt
@@ -0,0 +1,270 @@
+package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp
+
+import io.kotest.matchers.collections.shouldContain
+import io.kotest.matchers.shouldBe
+import io.ktor.client.HttpClient
+import io.ktor.client.plugins.sse.SSE
+import io.ktor.http.ContentType
+import io.ktor.http.HttpStatusCode
+import io.ktor.server.engine.embeddedServer
+import io.ktor.server.request.receiveText
+import io.ktor.server.response.header
+import io.ktor.server.response.respond
+import io.ktor.server.response.respondText
+import io.ktor.server.response.respondTextWriter
+import io.ktor.server.routing.Route
+import io.ktor.server.routing.delete
+import io.ktor.server.routing.get
+import io.ktor.server.routing.post
+import io.ktor.server.routing.routing
+import io.modelcontextprotocol.kotlin.sdk.client.Client
+import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions
+import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport
+import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
+import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject
+import io.modelcontextprotocol.kotlin.sdk.types.Implementation
+import io.modelcontextprotocol.kotlin.sdk.types.Tool
+import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
+import io.modelcontextprotocol.kotlin.test.utils.actualPort
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.put
+import kotlinx.serialization.json.putJsonObject
+import kotlin.test.Test
+import io.ktor.client.engine.cio.CIO as ClientCIO
+import io.ktor.server.cio.CIO as ServerCIO
+
+private const val SESSION_ID_HEADER = "mcp-session-id"
+private const val HOST = "127.0.0.1"
+private const val MCP_PATH = "/mcp"
+
+private val INITIALIZE_RESULT = """
+ {
+ "capabilities": {"tools": {"listChanged": false}},
+ "protocolVersion": "2025-03-26",
+ "serverInfo": {"name": "test-server", "version": "1.0.0"}
+ }
+""".trimIndent()
+
+private val TOOLS_LIST_RESULT = """
+ {
+ "tools": [
+ {
+ "name": "get_weather",
+ "title": "Weather Information Provider",
+ "description": "Get current weather information for a location",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "location": {"type": "string", "description": "City name or zip code"}
+ },
+ "required": ["location"]
+ },
+ "outputSchema": {
+ "type": "object",
+ "properties": {
+ "temperature": {"type": "number", "description": "Temperature, Celsius"}
+ },
+ "required": ["temperature"]
+ },
+ "_meta": {}
+ }
+ ]
+ }
+""".trimIndent()
+
+private const val PROGRESS_NOTIFICATION_1 =
+ """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}"""
+
+private const val PROGRESS_NOTIFICATION_2 =
+ """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":75,"total":100}}"""
+
+internal class StreamableHttpClientTransportIntegrationTest {
+
+ @Test
+ fun `client receives progress notifications via GET SSE and lists tools`(): Unit = runBlocking(Dispatchers.IO) {
+ val sessionId = "sid-progress"
+ runWithServer({
+ mcpPostRoute(sessionId, extraResults = mapOf("tools/list" to TOOLS_LIST_RESULT))
+ get(MCP_PATH) {
+ call.response.header(SESSION_ID_HEADER, sessionId)
+ call.respondTextWriter(contentType = ContentType.Text.EventStream) {
+ write(sseEvent(id = "1", data = PROGRESS_NOTIFICATION_1))
+ flush()
+ delay(200)
+ write(sseEvent(id = "2", data = PROGRESS_NOTIFICATION_2))
+ flush()
+ awaitCancellation()
+ }
+ }
+ }) { url ->
+ val client = newClient()
+ client.connect(StreamableHttpClientTransport(client = newHttpClient(), url = url))
+
+ val listToolsResult = client.listTools()
+
+ listToolsResult.tools shouldContain Tool(
+ name = "get_weather",
+ title = "Weather Information Provider",
+ description = "Get current weather information for a location",
+ inputSchema = ToolSchema(
+ properties = buildJsonObject {
+ putJsonObject("location") {
+ put("type", "string")
+ put("description", "City name or zip code")
+ }
+ },
+ required = listOf("location"),
+ ),
+ outputSchema = ToolSchema(
+ properties = buildJsonObject {
+ putJsonObject("temperature") {
+ put("type", "number")
+ put("description", "Temperature, Celsius")
+ }
+ },
+ required = listOf("temperature"),
+ ),
+ annotations = null,
+ meta = EmptyJsonObject,
+ )
+
+ client.close()
+ }
+ }
+
+ @Test
+ fun `terminateSession sends DELETE request and clears sessionId`(): Unit = runBlocking(Dispatchers.IO) {
+ val sessionId = "sid-terminate"
+ runWithServer({
+ mcpPostRoute(sessionId)
+ get(MCP_PATH) {
+ call.response.header(SESSION_ID_HEADER, sessionId)
+ call.respondTextWriter(contentType = ContentType.Text.EventStream) {
+ awaitCancellation()
+ }
+ }
+ delete(MCP_PATH) {
+ call.respond(HttpStatusCode.OK)
+ }
+ }) { url ->
+ val client = newClient()
+ val transport = StreamableHttpClientTransport(client = newHttpClient(), url = url)
+ client.connect(transport)
+
+ transport.sessionId shouldBe sessionId
+ transport.terminateSession()
+ transport.sessionId shouldBe null
+
+ client.close()
+ }
+ }
+
+ @Test
+ fun `client survives 405 on GET SSE`(): Unit = runBlocking(Dispatchers.IO) {
+ val sessionId = "sid-405"
+ runWithServer({
+ mcpPostRoute(sessionId, extraResults = mapOf("ping" to "{}"))
+ get(MCP_PATH) {
+ call.respondText(
+ text = "",
+ contentType = ContentType.Text.EventStream,
+ status = HttpStatusCode.MethodNotAllowed,
+ )
+ }
+ }) { url ->
+ val client = newClient()
+ client.connect(StreamableHttpClientTransport(client = newHttpClient(), url = url))
+
+ client.ping()
+
+ client.close()
+ }
+ }
+
+ @Test
+ fun `client survives non streaming JSON response on GET SSE`(): Unit = runBlocking(Dispatchers.IO) {
+ val sessionId = "sid-json"
+ runWithServer({
+ mcpPostRoute(sessionId, extraResults = mapOf("ping" to "{}"))
+ get(MCP_PATH) {
+ call.response.header(SESSION_ID_HEADER, sessionId)
+ call.respondText(
+ text = "",
+ contentType = ContentType.Application.Json,
+ status = HttpStatusCode.OK,
+ )
+ }
+ }) { url ->
+ val client = newClient()
+ client.connect(StreamableHttpClientTransport(client = newHttpClient(), url = url))
+
+ client.ping()
+
+ client.close()
+ }
+ }
+
+ private fun Route.mcpPostRoute(sessionId: String, extraResults: Map = emptyMap()) {
+ post(MCP_PATH) {
+ val body = call.receiveText()
+ val parsed = Json.parseToJsonElement(body).jsonObject
+ val method = parsed["method"]?.jsonPrimitive?.content
+ val idJson = parsed["id"]?.toString() ?: "null"
+ call.response.header(SESSION_ID_HEADER, sessionId)
+ when {
+ method == "initialize" -> call.respondText(
+ text = jsonRpcResult(idJson, INITIALIZE_RESULT),
+ contentType = ContentType.Application.Json,
+ status = HttpStatusCode.OK,
+ )
+
+ method == "notifications/initialized" -> call.respond(HttpStatusCode.Accepted)
+
+ method != null && method in extraResults -> call.respondText(
+ text = jsonRpcResult(idJson, extraResults.getValue(method)),
+ contentType = ContentType.Application.Json,
+ status = HttpStatusCode.OK,
+ )
+
+ else -> call.respond(HttpStatusCode.NotFound)
+ }
+ }
+ }
+
+ private suspend fun runWithServer(routes: Route.() -> Unit, block: suspend (url: String) -> Unit) {
+ val server = embeddedServer(ServerCIO, host = HOST, port = 0) {
+ routing { routes() }
+ }.startSuspend(wait = false)
+ try {
+ val port = server.actualPort()
+ block("http://$HOST:$port$MCP_PATH")
+ } finally {
+ server.stopSuspend(1000, 2000)
+ }
+ }
+
+ private fun newClient(): Client = Client(
+ clientInfo = Implementation(name = "client1", version = "1.0.0"),
+ options = ClientOptions(capabilities = ClientCapabilities()),
+ )
+
+ private fun newHttpClient(): HttpClient = HttpClient(ClientCIO) {
+ install(SSE)
+ }
+
+ private fun jsonRpcResult(idJson: String, resultJson: String): String =
+ """{"jsonrpc":"2.0","id":$idJson,"result":$resultJson}"""
+
+ private fun sseEvent(id: String, data: String): String = buildString {
+ append("event: message\n")
+ append("id: $id\n")
+ append("data: $data\n\n")
+ }
+}
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/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpSseReconnectTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpSseReconnectTest.kt
new file mode 100644
index 0000000..751f8dd
--- /dev/null
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/streamablehttp/StreamableHttpSseReconnectTest.kt
@@ -0,0 +1,171 @@
+package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp
+
+import io.kotest.matchers.nulls.shouldNotBeNull
+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.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.serialization.json.Json
+import kotlin.test.Test
+import kotlin.test.assertNotNull
+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"
+
+/**
+ * Integration tests for GET SSE stream reconnection using a real embedded CIO server.
+ *
+ * Verifies that the transport correctly evicts stale STANDALONE_SSE_STREAM_ID
+ * entries when a client reconnects after a disconnect, rather than silently
+ * rejecting the new stream.
+ */
+class StreamableHttpSseReconnectTest : AbstractStreamableHttpIntegrationTest() {
+
+ /**
+ * Verifies that after a GET SSE stream disconnects and the client
+ * immediately reconnects, the server evicts the stale stream mapping
+ * and allows the new stream to succeed.
+ */
+ @Test
+ fun `GET SSE reconnect after disconnect should succeed`(): Unit = runBlocking(Dispatchers.IO) {
+ var server: StreamableHttpTestServer? = null
+ var httpClient: HttpClient? = null
+
+ try {
+ server = initTestServer("reconnect-test")
+ val port = server.ktorServer.actualPort()
+ val mcpUrl = "http://$URL:$port/mcp"
+
+ httpClient = HttpClient(ClientCIO) { install(SSE) }
+
+ // Step 1: Initialize session via POST
+ val initResponse = httpClient.post(mcpUrl) {
+ contentType(ContentType.Application.Json)
+ header(
+ HttpHeaders.Accept,
+ "${ContentType.Application.Json}, ${ContentType.Text.EventStream}",
+ )
+ setBody(Json.encodeToString(buildInitPayload()))
+ }
+ initResponse.status shouldBe HttpStatusCode.OK
+ val sessionId = assertNotNull(initResponse.headers[SESSION_ID_HEADER])
+
+ // Step 2: Open GET SSE stream, consume the flush event, then disconnect
+ httpClient.prepareGet(mcpUrl) {
+ header(HttpHeaders.Accept, ContentType.Text.EventStream.toString())
+ header(SESSION_ID_HEADER, sessionId)
+ header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION)
+ }.execute { response ->
+ response.status shouldBe HttpStatusCode.OK
+ response.bodyAsChannel().readUTF8Line()
+ }
+
+ // Step 3: Immediately reconnect. The transport detects that the
+ // previous stream's coroutine is no longer active and evicts the
+ // stale mapping, allowing the new stream to succeed.
+ httpClient.prepareGet(mcpUrl) {
+ header(HttpHeaders.Accept, ContentType.Text.EventStream.toString())
+ header(SESSION_ID_HEADER, sessionId)
+ header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION)
+ }.execute { response ->
+ response.status shouldBe HttpStatusCode.OK
+ response.headers[SESSION_ID_HEADER] shouldBe sessionId
+
+ val channel = response.bodyAsChannel()
+ val firstLine = channel.readUTF8Line()
+ firstLine.shouldNotBeNull()
+ channel.isClosedForRead shouldBe false
+ }
+ } finally {
+ httpClient?.close()
+ server?.ktorServer?.stopSuspend(1000, 2000)
+ }
+ }
+
+ /**
+ * Verifies that a second concurrent GET SSE stream on the same session
+ * closes the old stream and takes over. The new stream should be live.
+ */
+ @Test
+ fun `concurrent GET SSE stream closes old stream and takes over`(): Unit = runBlocking(Dispatchers.IO) {
+ var server: StreamableHttpTestServer? = null
+ var httpClient: HttpClient? = null
+
+ try {
+ server = initTestServer("takeover-test")
+ val port = server.ktorServer.actualPort()
+ val mcpUrl = "http://$URL:$port/mcp"
+
+ httpClient = HttpClient(ClientCIO) { install(SSE) }
+
+ // Step 1: Initialize session via POST
+ val initResponse = httpClient.post(mcpUrl) {
+ contentType(ContentType.Application.Json)
+ header(
+ HttpHeaders.Accept,
+ "${ContentType.Application.Json}, ${ContentType.Text.EventStream}",
+ )
+ setBody(Json.encodeToString(buildInitPayload()))
+ }
+ initResponse.status shouldBe HttpStatusCode.OK
+ val sessionId = assertNotNull(initResponse.headers[SESSION_ID_HEADER])
+
+ // Step 2: Open first GET SSE stream and keep it open
+ httpClient.prepareGet(mcpUrl) {
+ header(HttpHeaders.Accept, ContentType.Text.EventStream.toString())
+ header(SESSION_ID_HEADER, sessionId)
+ header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION)
+ }.execute { firstResponse ->
+ firstResponse.status shouldBe HttpStatusCode.OK
+ firstResponse.bodyAsChannel().readUTF8Line()
+
+ // Step 3: Open a second GET — closes old stream, new one takes over
+ httpClient.prepareGet(mcpUrl) {
+ header(HttpHeaders.Accept, ContentType.Text.EventStream.toString())
+ header(SESSION_ID_HEADER, sessionId)
+ header(PROTOCOL_VERSION_HEADER, LATEST_PROTOCOL_VERSION)
+ }.execute { secondResponse ->
+ secondResponse.status shouldBe HttpStatusCode.OK
+ secondResponse.headers[SESSION_ID_HEADER] shouldBe sessionId
+
+ // New stream is alive
+ val secondChannel = secondResponse.bodyAsChannel()
+ val firstLine = secondChannel.readUTF8Line()
+ firstLine.shouldNotBeNull()
+ secondChannel.isClosedForRead shouldBe false
+ }
+ }
+ } finally {
+ httpClient?.close()
+ server?.ktorServer?.stopSuspend(1000, 2000)
+ }
+ }
+
+ private fun buildInitPayload(): JSONRPCRequest = InitializeRequest(
+ InitializeRequestParams(
+ protocolVersion = LATEST_PROTOCOL_VERSION,
+ capabilities = ClientCapabilities(),
+ clientInfo = Implementation(name = "reconnect-test-client", version = "1.0.0"),
+ ),
+ ).toJSON()
+}
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/ClientConnectionTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt
index ee8953a..581e5ae 100644
--- a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnectionTest.kt
@@ -9,6 +9,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams
import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification
import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotificationParams
+import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams
import io.modelcontextprotocol.kotlin.sdk.types.ListRootsRequest
@@ -61,6 +62,7 @@ class ClientConnectionTest : AbstractServerFeaturesTest() {
override fun getClientCapabilities(): ClientCapabilities = ClientCapabilities(
roots = ClientCapabilities.Roots(listChanged = true),
+ elicitation = ClientCapabilities.Elicitation(url = EmptyJsonObject),
)
private val sampleRoots = listOf(Root("file:///project", "Project Root"))
diff --git a/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt
new file mode 100644
index 0000000..f161d8e
--- /dev/null
+++ b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt
@@ -0,0 +1,251 @@
+package io.modelcontextprotocol.kotlin.sdk.server
+
+import io.modelcontextprotocol.kotlin.sdk.client.Client
+import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions
+import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport
+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.EmptyJsonObject
+import io.modelcontextprotocol.kotlin.sdk.types.Implementation
+import io.modelcontextprotocol.kotlin.sdk.types.IncludeContext
+import io.modelcontextprotocol.kotlin.sdk.types.Method
+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.StopReason
+import io.modelcontextprotocol.kotlin.sdk.types.TextContent
+import io.modelcontextprotocol.kotlin.sdk.types.Tool
+import io.modelcontextprotocol.kotlin.sdk.types.ToolChoice
+import io.modelcontextprotocol.kotlin.sdk.types.ToolResultContent
+import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
+import io.modelcontextprotocol.kotlin.sdk.types.ToolUseContent
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.buildJsonObject
+import org.junit.jupiter.api.assertDoesNotThrow
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class SamplingTest {
+
+ private val dummyTool = Tool(
+ name = "t",
+ inputSchema = ToolSchema(properties = buildJsonObject { }, required = emptyList()),
+ )
+
+ private val weatherTool = Tool(
+ name = "get_weather",
+ description = "Return the current temperature in Celsius.",
+ inputSchema = ToolSchema(
+ properties = buildJsonObject {
+ put("location", buildJsonObject { put("type", JsonPrimitive("string")) })
+ },
+ required = listOf("location"),
+ ),
+ )
+
+ private val minimalMessages = listOf(SamplingMessage(Role.User, TextContent("hi")))
+
+ /**
+ * Builds a connected [Server]+[Client] pair using [InMemoryTransport].
+ *
+ * @param clientCapabilities the capabilities the client advertises during initialize
+ * @param samplingHandler the handler the client uses to respond to sampling requests
+ * @return Pair of (server, sessionId) ready for [Server.createMessage] calls.
+ */
+ private fun buildPair(
+ clientCapabilities: ClientCapabilities = ClientCapabilities(
+ sampling = ClientCapabilities.Sampling(),
+ ),
+ samplingHandler: (CreateMessageRequest) -> CreateMessageResult = { _ ->
+ CreateMessageResult(role = Role.Assistant, content = TextContent("ok"), model = "m")
+ },
+ ): Pair {
+ val server = Server(
+ serverInfo = Implementation(name = "srv", version = "1.0"),
+ options = ServerOptions(capabilities = ServerCapabilities()),
+ )
+
+ val client = Client(
+ clientInfo = Implementation(name = "cli", version = "1.0"),
+ options = ClientOptions(capabilities = clientCapabilities),
+ )
+
+ client.setRequestHandler(Method.Defined.SamplingCreateMessage) { req, _ ->
+ samplingHandler(req)
+ }
+
+ val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair()
+
+ var sessionId: String? = null
+ runBlocking {
+ val sessionDeferred = CompletableDeferred()
+ launch { client.connect(clientTransport) }
+ launch {
+ val session = server.createSession(serverTransport)
+ sessionDeferred.complete(session.sessionId)
+ }
+ sessionId = sessionDeferred.await()
+ }
+
+ return Pair(server, checkNotNull(sessionId))
+ }
+
+ // ============================================================================
+ // Server.createMessage — capability enforcement (SEP-1577)
+ // ============================================================================
+
+ @Test
+ fun `tools field rejected when client has no sampling tools capability`() {
+ val (server, sessionId) = buildPair()
+ assertFailsWith {
+ runBlocking {
+ server.createMessage(
+ sessionId = sessionId,
+ params = CreateMessageRequest(
+ params = CreateMessageRequestParams(
+ maxTokens = 100,
+ messages = minimalMessages,
+ tools = listOf(dummyTool),
+ ),
+ ),
+ )
+ }
+ }
+ }
+
+ @Test
+ fun `toolChoice field rejected when client has no sampling tools capability`() {
+ val (server, sessionId) = buildPair()
+ assertFailsWith {
+ runBlocking {
+ server.createMessage(
+ sessionId = sessionId,
+ params = CreateMessageRequest(
+ params = CreateMessageRequestParams(
+ maxTokens = 100,
+ messages = minimalMessages,
+ toolChoice = ToolChoice(),
+ ),
+ ),
+ )
+ }
+ }
+ }
+
+ @Test
+ fun `includeContext with no sampling context capability succeeds with a warning`() {
+ val (server, sessionId) = buildPair()
+ assertDoesNotThrow {
+ runBlocking {
+ server.createMessage(
+ sessionId = sessionId,
+ params = CreateMessageRequest(
+ params = CreateMessageRequestParams(
+ maxTokens = 100,
+ messages = minimalMessages,
+ includeContext = IncludeContext.ThisServer,
+ ),
+ ),
+ )
+ }
+ }
+ }
+
+ // ============================================================================
+ // End-to-end tool-loop integration
+ // ============================================================================
+
+ @Test
+ fun `server sends tools, client returns tool_use then final text`() {
+ var turn = 0
+ val (server, sessionId) = buildPair(
+ clientCapabilities = ClientCapabilities(
+ sampling = ClientCapabilities.Sampling(tools = EmptyJsonObject),
+ ),
+ samplingHandler = { _ ->
+ turn++
+ if (turn == 1) {
+ CreateMessageResult(
+ role = Role.Assistant,
+ content = listOf(
+ TextContent("Let me check."),
+ ToolUseContent(
+ id = "call_1",
+ name = "get_weather",
+ input = buildJsonObject { put("location", JsonPrimitive("London")) },
+ ),
+ ),
+ model = "test",
+ stopReason = StopReason.ToolUse,
+ )
+ } else {
+ CreateMessageResult(
+ role = Role.Assistant,
+ content = TextContent("The temperature in London is 20°C."),
+ model = "test",
+ stopReason = StopReason.EndTurn,
+ )
+ }
+ },
+ )
+
+ runBlocking {
+ val messages = mutableListOf(
+ SamplingMessage(Role.User, TextContent("What is the weather in London?")),
+ )
+
+ // — Turn 1: server sends tools, expects tool_use stop reason
+ val first = server.createMessage(
+ sessionId = sessionId,
+ params = CreateMessageRequest(
+ params = CreateMessageRequestParams(
+ maxTokens = 256,
+ messages = messages.toList(),
+ tools = listOf(weatherTool),
+ toolChoice = ToolChoice(mode = ToolChoice.Mode.Auto),
+ ),
+ ),
+ )
+ assertEquals(StopReason.ToolUse, first.stopReason)
+ assertEquals(2, first.content.size)
+
+ // — Append assistant turn and inject tool result
+ messages.add(SamplingMessage(Role.Assistant, first.content))
+ val toolUse = first.content.filterIsInstance().single()
+ messages.add(
+ SamplingMessage(
+ Role.User,
+ ToolResultContent(
+ toolUseId = toolUse.id,
+ content = listOf(TextContent("""{"tempC":20}""")),
+ ),
+ ),
+ )
+
+ // — Turn 2: server sends updated history, expects final text
+ val second = server.createMessage(
+ sessionId = sessionId,
+ params = CreateMessageRequest(
+ params = CreateMessageRequestParams(
+ maxTokens = 256,
+ messages = messages.toList(),
+ tools = listOf(weatherTool),
+ toolChoice = ToolChoice(mode = ToolChoice.Mode.Auto),
+ ),
+ ),
+ )
+ assertEquals(StopReason.EndTurn, second.stopReason)
+ assertEquals(1, second.content.size)
+ val text = second.content.single() as TextContent
+ assertEquals("The temperature in London is 20°C.", text.text)
+
+ server.close()
+ }
+ }
+}
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/ServerSessionInitializeTest.kt b/integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionInitializeTest.kt
index 5031a9c..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()
@@ -65,6 +83,30 @@ 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 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/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 c2e8201..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
@@ -98,6 +101,52 @@ class ServerToolsTest : AbstractServerFeaturesTest() {
assertFalse(toolListChangedNotificationReceived, "No notification should be sent when tool doesn't exist")
}
+ @Test
+ fun `addTool should succeed with non-conforming tool name`() = runTest {
+ server.addTool("my invalid tool!", "Tool with non-conforming name", ToolSchema()) {
+ CallToolResult(listOf(TextContent("It works")))
+ }
+
+ val result = server.removeTool("my invalid tool!")
+ 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/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,
diff --git a/integration-test/src/jvmTest/typescript/package-lock.json b/integration-test/src/jvmTest/typescript/package-lock.json
index dd0c9b4..ffb53fc 100644
--- a/integration-test/src/jvmTest/typescript/package-lock.json
+++ b/integration-test/src/jvmTest/typescript/package-lock.json
@@ -8,23 +8,23 @@
"name": "mcp-typescript-test-sdk",
"version": "1.0.0",
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/sdk": "^1.30.0",
"express": "^5.2.1",
- "zod": "^4.3.6"
+ "zod": "^4.6.2"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
- "@types/node": "^25.5.2",
+ "@types/node": "^26.5.1",
"cors": "^2.8.6",
- "tsx": "^4.21.0",
- "typescript": "^6.0.2"
+ "tsx": "^4.23.13",
+ "typescript": "^7.0.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
- "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"cpu": [
"ppc64"
],
@@ -39,9 +39,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
- "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"cpu": [
"arm"
],
@@ -56,9 +56,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
- "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"cpu": [
"arm64"
],
@@ -73,9 +73,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
- "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"cpu": [
"x64"
],
@@ -90,9 +90,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
- "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"cpu": [
"arm64"
],
@@ -107,9 +107,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
- "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"cpu": [
"x64"
],
@@ -124,9 +124,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
- "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"cpu": [
"arm64"
],
@@ -141,9 +141,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
- "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"cpu": [
"x64"
],
@@ -158,9 +158,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
- "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"cpu": [
"arm"
],
@@ -175,9 +175,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
- "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"cpu": [
"arm64"
],
@@ -192,9 +192,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
- "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"cpu": [
"ia32"
],
@@ -209,9 +209,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
- "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"cpu": [
"loong64"
],
@@ -226,9 +226,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
- "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"cpu": [
"mips64el"
],
@@ -243,9 +243,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
- "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"cpu": [
"ppc64"
],
@@ -260,9 +260,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
- "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"cpu": [
"riscv64"
],
@@ -277,9 +277,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
- "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"cpu": [
"s390x"
],
@@ -294,9 +294,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
- "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"cpu": [
"x64"
],
@@ -311,9 +311,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
- "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"cpu": [
"arm64"
],
@@ -328,9 +328,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
- "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"cpu": [
"x64"
],
@@ -345,9 +345,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
- "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"cpu": [
"arm64"
],
@@ -362,9 +362,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
- "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"cpu": [
"x64"
],
@@ -379,9 +379,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
- "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"cpu": [
"arm64"
],
@@ -396,9 +396,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
- "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"cpu": [
"x64"
],
@@ -413,9 +413,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
- "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"cpu": [
"arm64"
],
@@ -430,9 +430,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
- "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"cpu": [
"ia32"
],
@@ -447,9 +447,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
- "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"cpu": [
"x64"
],
@@ -464,24 +464,24 @@
}
},
"node_modules/@hono/node-server": {
- "version": "1.19.13",
- "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz",
- "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.0.tgz",
+ "integrity": "sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==",
"license": "MIT",
"engines": {
- "node": ">=18.14.1"
+ "node": ">=20"
},
"peerDependencies": {
"hono": "^4"
}
},
"node_modules/@modelcontextprotocol/sdk": {
- "version": "1.29.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
- "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==",
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
+ "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
"license": "MIT",
"dependencies": {
- "@hono/node-server": "^1.19.9",
+ "@hono/node-server": "^1.19.9 || ^2.0.5",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"content-type": "^1.0.5",
@@ -579,13 +579,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.5.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
- "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
+ "version": "26.5.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz",
+ "integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.18.0"
+ "undici-types": "~8.9.0"
}
},
"node_modules/@types/qs": {
@@ -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",
@@ -670,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"
@@ -693,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",
@@ -876,9 +1229,9 @@
}
},
"node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -888,9 +1241,9 @@
}
},
"node_modules/esbuild": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
- "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -901,32 +1254,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.2",
- "@esbuild/android-arm": "0.27.2",
- "@esbuild/android-arm64": "0.27.2",
- "@esbuild/android-x64": "0.27.2",
- "@esbuild/darwin-arm64": "0.27.2",
- "@esbuild/darwin-x64": "0.27.2",
- "@esbuild/freebsd-arm64": "0.27.2",
- "@esbuild/freebsd-x64": "0.27.2",
- "@esbuild/linux-arm": "0.27.2",
- "@esbuild/linux-arm64": "0.27.2",
- "@esbuild/linux-ia32": "0.27.2",
- "@esbuild/linux-loong64": "0.27.2",
- "@esbuild/linux-mips64el": "0.27.2",
- "@esbuild/linux-ppc64": "0.27.2",
- "@esbuild/linux-riscv64": "0.27.2",
- "@esbuild/linux-s390x": "0.27.2",
- "@esbuild/linux-x64": "0.27.2",
- "@esbuild/netbsd-arm64": "0.27.2",
- "@esbuild/netbsd-x64": "0.27.2",
- "@esbuild/openbsd-arm64": "0.27.2",
- "@esbuild/openbsd-x64": "0.27.2",
- "@esbuild/openharmony-arm64": "0.27.2",
- "@esbuild/sunos-x64": "0.27.2",
- "@esbuild/win32-arm64": "0.27.2",
- "@esbuild/win32-ia32": "0.27.2",
- "@esbuild/win32-x64": "0.27.2"
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
}
},
"node_modules/escape-html": {
@@ -1009,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"
@@ -1033,9 +1387,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
+ "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
"funding": [
{
"type": "github",
@@ -1147,19 +1501,6 @@
"node": ">= 0.4"
}
},
- "node_modules/get-tsconfig": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
- "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "resolve-pkg-maps": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
- }
- },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -1185,9 +1526,9 @@
}
},
"node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -1197,9 +1538,9 @@
}
},
"node_modules/hono": {
- "version": "4.12.12",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
- "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
+ "version": "4.13.7",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz",
+ "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -1248,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.4.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz",
+ "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==",
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -1461,12 +1802,13 @@
}
},
"node_modules/qs": {
- "version": "6.14.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
- "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
+ "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"license": "BSD-3-Clause",
"dependencies": {
- "side-channel": "^1.1.0"
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
@@ -1508,16 +1850,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/resolve-pkg-maps": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
- "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
- }
- },
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@@ -1613,14 +1945,14 @@
}
},
"node_modules/side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
@@ -1632,13 +1964,13 @@
}
},
"node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
+ "object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
@@ -1703,14 +2035,13 @@
}
},
"node_modules/tsx": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
- "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "version": "4.23.13",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz",
+ "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "esbuild": "~0.27.0",
- "get-tsconfig": "^4.7.5"
+ "esbuild": "~0.28.0"
},
"bin": {
"tsx": "dist/cli.mjs"
@@ -1723,37 +2054,75 @@
}
},
"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": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
- "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
+ "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": {
- "version": "7.18.2",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
- "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
+ "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
"dev": true,
"license": "MIT"
},
@@ -1797,9 +2166,9 @@
"license": "ISC"
},
"node_modules/zod": {
- "version": "4.3.6",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
- "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.2.tgz",
+ "integrity": "sha512-lh5RCAGFa1Cm2hjtNwLQhSs/AsqdWnTQaBER9fEwN/88pSh7KOtJavtBx/0VlkN/uFd61SwYmljLMDAsHlvzBQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/integration-test/src/jvmTest/typescript/package.json b/integration-test/src/jvmTest/typescript/package.json
index 20249d0..b0147e4 100644
--- a/integration-test/src/jvmTest/typescript/package.json
+++ b/integration-test/src/jvmTest/typescript/package.json
@@ -9,16 +9,16 @@
"server:http": "tsx server/http-server.ts"
},
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/sdk": "^1.30.0",
"express": "^5.2.1",
- "zod": "^4.3.6"
+ "zod": "^4.6.2"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
- "@types/node": "^25.5.2",
+ "@types/node": "^26.5.1",
"cors": "^2.8.6",
- "tsx": "^4.21.0",
- "typescript": "^6.0.2"
+ "tsx": "^4.23.13",
+ "typescript": "^7.0.2"
}
}
diff --git a/kotlin-sdk-client/api/kotlin-sdk-client.api b/kotlin-sdk-client/api/kotlin-sdk-client.api
index da0e752..d856f5b 100644
--- a/kotlin-sdk-client/api/kotlin-sdk-client.api
+++ b/kotlin-sdk-client/api/kotlin-sdk-client.api
@@ -34,6 +34,7 @@ public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextp
public final fun removeRoot (Ljava/lang/String;)Z
public final fun removeRoots (Ljava/util/List;)I
public final fun sendRootsListChanged (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public final fun setElicitationCompleteHandler (Lkotlin/jvm/functions/Function1;)V
public final fun setElicitationHandler (Lkotlin/jvm/functions/Function1;)V
public final fun setLoggingLevel (Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun setLoggingLevel$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
@@ -41,6 +42,7 @@ public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextp
public static synthetic fun subscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/SubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun unsubscribeResource (Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun unsubscribeResource$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ protected fun wrapRequestHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function3;
}
public final class io/modelcontextprotocol/kotlin/sdk/client/ClientKt {
@@ -50,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;
}
@@ -100,8 +102,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/client/StdioClientTranspor
}
public final class io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport {
- public fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;)V
- public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;ILkotlin/jvm/functions/Function1;)V
+ public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/client/ReconnectionOptions;ILkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getProtocolVersion ()Ljava/lang/String;
diff --git a/kotlin-sdk-client/build.gradle.kts b/kotlin-sdk-client/build.gradle.kts
index f50266c..fbb1300 100644
--- a/kotlin-sdk-client/build.gradle.kts
+++ b/kotlin-sdk-client/build.gradle.kts
@@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
id("mcp.multiplatform")
+ id("mcp.detekt")
id("mcp.publishing")
id("mcp.dokka")
alias(libs.plugins.kotlinx.binary.compatibility.validator)
@@ -14,10 +15,8 @@ kotlin {
iosArm64()
iosX64()
iosSimulatorArm64()
- watchosX64()
watchosArm64()
watchosSimulatorArm64()
- tvosX64()
tvosArm64()
tvosSimulatorArm64()
js {
@@ -56,7 +55,6 @@ kotlin {
implementation(libs.ktor.client.apache5)
implementation(libs.mockk)
implementation(libs.junit.jupiter.params)
- implementation(libs.mokksy)
implementation(dependencies.platform(libs.netty.bom))
runtimeOnly(libs.slf4j.simple)
}
diff --git a/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml
deleted file mode 100644
index 5cddbe1..0000000
--- a/kotlin-sdk-client/detekt-baseline-commonMainSourceSet.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- CyclomaticComplexMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize
- LongMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize
- MaxLineLength:StdioClientTransport.kt:StdioClientTransport$*
- ThrowsCount:SseClientTransport.kt:SseClientTransport$private suspend fun CoroutineScope.collectMessages
- ThrowsCount:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$@Suppress("ReturnCount", "CyclomaticComplexMethod") override suspend fun performSend
- TooGenericExceptionCaught:Client.kt:Client$error: Throwable
- TooGenericExceptionCaught:SseClientTransport.kt:SseClientTransport$e: Throwable
- TooGenericExceptionCaught:StdioClientTransport.kt:StdioClientTransport$e: Throwable
-
-
diff --git a/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml b/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml
deleted file mode 100644
index cab4322..0000000
--- a/kotlin-sdk-client/detekt-baseline-commonTestSourceSet.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}"""
- MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}"""
-
-
diff --git a/kotlin-sdk-client/detekt-baseline-main.xml b/kotlin-sdk-client/detekt-baseline-main.xml
index 8411ab0..c34e75c 100644
--- a/kotlin-sdk-client/detekt-baseline-main.xml
+++ b/kotlin-sdk-client/detekt-baseline-main.xml
@@ -2,18 +2,6 @@
- CyclomaticComplexMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize
- InjectDispatcher:StdioClientTransport.kt:StdioClientTransport$Default
- InjectDispatcher:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$Default
- LongMethod:StdioClientTransport.kt:StdioClientTransport$override suspend fun initialize
- MaxLineLength:StdioClientTransport.kt:StdioClientTransport$*
- NoNameShadowing:StdioClientTransport.kt:StdioClientTransport$source
- ThrowsCount:SseClientTransport.kt:SseClientTransport$private suspend fun CoroutineScope.collectMessages
- ThrowsCount:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$@Suppress("ReturnCount", "CyclomaticComplexMethod") override suspend fun performSend
- TooGenericExceptionCaught:Client.kt:Client$error: Throwable
- TooGenericExceptionCaught:SseClientTransport.kt:SseClientTransport$e: Throwable
- TooGenericExceptionCaught:StdioClientTransport.kt:StdioClientTransport$e: Throwable
- UseCheckOrError:Client.kt:Client$throw IllegalStateException( "Server does not support resource subscriptions (required for $method)", )
- UseCheckOrError:Client.kt:Client$throw IllegalStateException("Client does not support roots capability.")
+ LoopWithTooManyJumpStatements:StreamableHttpClientTransport.kt:StreamableHttpClientTransport$while
diff --git a/kotlin-sdk-client/detekt-baseline-test.xml b/kotlin-sdk-client/detekt-baseline-test.xml
deleted file mode 100644
index dd6adfa..0000000
--- a/kotlin-sdk-client/detekt-baseline-test.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- AbstractClassCanBeConcreteClass:AbstractStreamableHttpClientTest.kt:AbstractStreamableHttpClientTest$AbstractStreamableHttpClientTest
- ForbiddenComment:StreamableHttpClientTest.kt:StreamableHttpClientTest$// TODO: how to get notifications via Client API?
- LongParameterList:MockMcp.kt:MockMcp$fun handleJSONRPCRequest
- LongParameterList:MockMcp.kt:MockMcp$fun handleWithResult
- LongParameterList:MockMcp.kt:MockMcp$fun onInitialize
- MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}"""
- MaxLineLength:StreamableHttpClientTransportTest.kt:StreamableHttpClientTransportTest$"""data: {"jsonrpc":"2.0","method":"notifications/resumed","params":{"fromToken":"$lastEventIdSent"}}"""
- UseOrEmpty:ClientMetaParameterTest.kt:ClientMetaParameterTest$exception.message ?: ""
-
-
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 34b9a5b..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
@@ -4,6 +4,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi
import io.modelcontextprotocol.kotlin.sdk.shared.Protocol
import io.modelcontextprotocol.kotlin.sdk.shared.ProtocolOptions
+import io.modelcontextprotocol.kotlin.sdk.shared.RequestHandlerExtra
import io.modelcontextprotocol.kotlin.sdk.shared.RequestOptions
import io.modelcontextprotocol.kotlin.sdk.shared.Transport
import io.modelcontextprotocol.kotlin.sdk.types.BooleanSchema
@@ -13,10 +14,12 @@ import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult
import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
import io.modelcontextprotocol.kotlin.sdk.types.CompleteRequest
import io.modelcontextprotocol.kotlin.sdk.types.CompleteResult
+import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest
import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequest
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams
import io.modelcontextprotocol.kotlin.sdk.types.ElicitResult
+import io.modelcontextprotocol.kotlin.sdk.types.ElicitationCompleteNotification
import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult
@@ -45,7 +48,9 @@ import io.modelcontextprotocol.kotlin.sdk.types.PingRequest
import io.modelcontextprotocol.kotlin.sdk.types.PrimitiveSchemaDefinition
import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest
import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult
+import io.modelcontextprotocol.kotlin.sdk.types.Request
import io.modelcontextprotocol.kotlin.sdk.types.RequestMeta
+import io.modelcontextprotocol.kotlin.sdk.types.RequestResult
import io.modelcontextprotocol.kotlin.sdk.types.Root
import io.modelcontextprotocol.kotlin.sdk.types.RootsListChangedNotification
import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS
@@ -59,6 +64,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.TitledSingleSelectEnumSchema
import io.modelcontextprotocol.kotlin.sdk.types.UnsubscribeRequest
import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema
import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema
+import io.modelcontextprotocol.kotlin.sdk.types.supportsUrl
import io.modelcontextprotocol.kotlin.sdk.types.toJson
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.getAndUpdate
@@ -66,11 +72,14 @@ import kotlinx.atomicfu.update
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 {}
@@ -79,12 +88,17 @@ private val logger = KotlinLogging.logger {}
* Options for configuring the MCP client.
*
* @property capabilities The capabilities this client supports.
- * @property enforceStrictCapabilities Whether to strictly enforce capabilities when interacting with the server.
+ * @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,
@@ -121,7 +135,6 @@ public suspend fun mcpClient(
* @param clientInfo Information about the client implementation (name, version).
* @param options Configuration options for this client.
*/
-@Suppress("TooManyFunctions")
public open class Client(private val clientInfo: Implementation, options: ClientOptions = ClientOptions()) :
Protocol(options) {
@@ -171,6 +184,7 @@ public open class Client(private val clientInfo: Implementation, options: Client
"prompts" -> caps?.prompts != null
"resources" -> caps?.resources != null
"tools" -> caps?.tools != null
+ "tasks" -> caps?.tasks != null
else -> true
}
@@ -209,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()
@@ -235,10 +250,15 @@ public open class Client(private val clientInfo: Implementation, options: Client
Method.Defined.PromptsGet,
Method.Defined.PromptsList,
- Method.Defined.CompletionComplete,
-> {
checkNotNull(serverCapabilities?.prompts) {
- throw IllegalStateException("Server does not support prompts (required for $method)")
+ "Server does not support prompts (required for $method)"
+ }
+ }
+
+ Method.Defined.CompletionComplete -> {
+ checkNotNull(serverCapabilities?.completions) {
+ "Server does not support completions (required for $method)"
}
}
@@ -251,10 +271,10 @@ public open class Client(private val clientInfo: Implementation, options: Client
val resCaps = serverCapabilities?.resources
?: error("Server does not support resources (required for $method)")
- if (method == Method.Defined.ResourcesSubscribe && resCaps.subscribe != true) {
- throw IllegalStateException(
- "Server does not support resource subscriptions (required for $method)",
- )
+ if (method == Method.Defined.ResourcesSubscribe) {
+ check(resCaps.subscribe == true) {
+ "Server does not support resource subscriptions (required for $method)"
+ }
}
}
@@ -264,6 +284,12 @@ public open class Client(private val clientInfo: Implementation, options: Client
}
}
+ Method.Defined.TasksGet,
+ Method.Defined.TasksResult,
+ Method.Defined.TasksList,
+ Method.Defined.TasksCancel,
+ -> assertTasksCapabilityForMethod(method)
+
Method.Defined.Initialize, Method.Defined.Ping -> {
// No specific capability required
}
@@ -274,6 +300,24 @@ public open class Client(private val clientInfo: Implementation, options: Client
}
}
+ private fun assertTasksCapabilityForMethod(method: Method) {
+ val tasks = serverCapabilities?.tasks
+ ?: error("Server does not support tasks (required for $method)")
+ when (method) {
+ Method.Defined.TasksList -> checkNotNull(tasks.list) {
+ "Server does not support listing tasks (required for $method)"
+ }
+
+ Method.Defined.TasksCancel -> checkNotNull(tasks.cancel) {
+ "Server does not support cancelling tasks (required for $method)"
+ }
+
+ else -> {
+ // TasksGet, TasksResult: base tasks capability suffices.
+ }
+ }
+ }
+
override fun assertNotificationCapability(method: Method) {
when (method) {
Method.Defined.NotificationsRootsListChanged -> {
@@ -282,6 +326,12 @@ public open class Client(private val clientInfo: Implementation, options: Client
}
}
+ Method.Defined.NotificationsTasksStatus -> {
+ checkNotNull(capabilities.tasks) {
+ "Client does not support tasks (required for $method)"
+ }
+ }
+
Method.Defined.NotificationsInitialized,
Method.Defined.NotificationsCancelled,
Method.Defined.NotificationsProgress,
@@ -323,6 +373,26 @@ public open class Client(private val clientInfo: Implementation, options: Client
}
}
+ /**
+ * Wraps incoming-request handlers with SEP-1577 client-side enforcement.
+ *
+ * For `sampling/createMessage`: if the incoming request carries `tools` or
+ * `toolChoice` but this client did not advertise [ClientCapabilities.Sampling.tools],
+ * the wrapper throws an [McpException] with JSON-RPC error code `InvalidParams`
+ * before the user-supplied handler runs. Matches the TypeScript SDK wrapper in
+ * `Client.setRequestHandler`.
+ */
+ override fun wrapRequestHandler(
+ method: Method,
+ block: suspend (T, RequestHandlerExtra) -> RequestResult?,
+ ): suspend (T, RequestHandlerExtra) -> RequestResult? {
+ if (method != Method.Defined.SamplingCreateMessage) return block
+ return { request, extra ->
+ (request as? CreateMessageRequest)?.let { validateSamplingToolsCapability(it, capabilities) }
+ block(request, extra)
+ }
+ }
+
/**
* Sends a ping request to the server to check connectivity.
*
@@ -337,7 +407,7 @@ public open class Client(private val clientInfo: Implementation, options: Client
* @param params The completion request parameters.
* @param options Optional request options.
* @return The completion result returned by the server, or `null` if none.
- * @throws IllegalStateException If the server does not support prompts or completion.
+ * @throws IllegalStateException If the server does not support completions.
*/
public suspend fun complete(params: CompleteRequest, options: RequestOptions? = null): CompleteResult =
request(params, options)
@@ -532,9 +602,8 @@ public open class Client(private val clientInfo: Implementation, options: Client
* @throws IllegalStateException If the client does not support roots.
*/
public fun removeRoot(uri: String): Boolean {
- if (capabilities.roots == null) {
- logger.error { "Failed to remove root '$uri': Client does not support roots capability" }
- throw IllegalStateException("Client does not support roots capability.")
+ checkNotNull(capabilities.roots) {
+ "Client does not support roots capability."
}
logger.info { "Removing root: $uri" }
val oldMap = roots.getAndUpdate { current -> current.remove(uri) }
@@ -590,9 +659,18 @@ public open class Client(private val clientInfo: Implementation, options: Client
/**
* Sets the elicitation handler.
*
- * When the handler returns [ElicitResult.Action.Accept], any properties missing from
+ * The handler receives both form-mode ([ElicitRequestFormParams]) and URL-mode
+ * ([io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestURLParams]) requests;
+ * branch on `request.params` to tell them apart. For URL mode,
+ * the host application must obtain explicit user consent and display the target domain before
+ * navigating — the SDK never opens or validates the URL — and should return
+ * [ElicitResult.Action.Decline] or [ElicitResult.Action.Cancel] when it cannot or will not proceed.
+ * A URL-mode [ElicitResult.Action.Accept] only signals consent; the outcome arrives out-of-band via
+ * [setElicitationCompleteHandler].
+ *
+ * When a form-mode handler returns [ElicitResult.Action.Accept], any properties missing from
* [ElicitResult.content] are automatically populated with default values defined in the
- * elicitation schema.
+ * elicitation schema. URL-mode responses carry no content.
*
* @param handler The elicitation handler.
* @throws IllegalStateException if the client does not support elicitation.
@@ -610,9 +688,37 @@ public open class Client(private val clientInfo: Implementation, options: Client
}
}
+ /**
+ * Sets the handler invoked when the server reports that a URL-mode elicitation has completed.
+ *
+ * The handler is called for every `notifications/elicitation/complete` notification. Because the
+ * server only sends this for an out-of-band (URL-mode) interaction, the client must support url-mode
+ * elicitation. The client is responsible for correlating the notification's `elicitationId` with a
+ * pending elicitation, ignoring unknown or already-completed identifiers, and providing a manual way
+ * to continue if a notification never arrives.
+ *
+ * @param handler Invoked with each completion notification.
+ * @throws IllegalStateException if the client does not support url-mode elicitation.
+ */
+ public fun setElicitationCompleteHandler(handler: (ElicitationCompleteNotification) -> Unit) {
+ check(capabilities.elicitation.supportsUrl) {
+ logger.error {
+ "Failed to set elicitation-complete handler: client does not support url-mode elicitation"
+ }
+ "Client does not support url-mode elicitation."
+ }
+ logger.info { "Setting the elicitation-complete handler" }
+
+ setNotificationHandler(
+ Method.Defined.NotificationsElicitationComplete,
+ ) { notification ->
+ handler(notification)
+ CompletableDeferred(Unit)
+ }
+ }
+
// --- Internal Handlers ---
- @Suppress("ReturnCount")
private fun applyElicitationDefaults(request: ElicitRequest, result: ElicitResult): ElicitResult {
if (result.action != ElicitResult.Action.Accept) return result
val formParams = request.params as? ElicitRequestFormParams ?: return result
diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SamplingValidation.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SamplingValidation.kt
new file mode 100644
index 0000000..1290cb0
--- /dev/null
+++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SamplingValidation.kt
@@ -0,0 +1,24 @@
+package io.modelcontextprotocol.kotlin.sdk.client
+
+import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
+import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest
+import io.modelcontextprotocol.kotlin.sdk.types.McpException
+import io.modelcontextprotocol.kotlin.sdk.types.RPCError
+
+/**
+ * Validates SEP-1577 client-side enforcement: an incoming `sampling/createMessage`
+ * request that carries `tools` or `toolChoice` requires the client to have advertised
+ * the `sampling.tools` sub-capability. When the capability is missing, throws an
+ * [McpException] with JSON-RPC error code `InvalidParams`, matching TypeScript SDK
+ * client enforcement.
+ */
+internal fun validateSamplingToolsCapability(request: CreateMessageRequest, capabilities: ClientCapabilities) {
+ val params = request.params
+ if (params.tools == null && params.toolChoice == null) return
+ if (capabilities.sampling?.tools != null) return
+ val field = if (params.tools != null) "tools" else "toolChoice"
+ throw McpException(
+ code = RPCError.ErrorCode.INVALID_PARAMS,
+ message = "Client does not support sampling with tools but request contains $field parameter",
+ )
+}
diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt
index 3c9d88b..0295eb9 100644
--- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt
+++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SseClientTransport.kt
@@ -24,11 +24,12 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
+import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.time.Duration
@@ -135,6 +136,7 @@ public class SseClientTransport(
throw e
} finally {
closeResources()
+ invokeOnCloseCallback()
}
}
@@ -171,13 +173,15 @@ public class SseClientTransport(
}
override suspend fun closeResources() {
- job?.cancelAndJoin()
- try {
- if (::session.isInitialized) session.cancel()
- if (::scope.isInitialized) scope.cancel()
- endpoint.cancel()
- } catch (e: Throwable) {
- _onError(e)
+ withContext(NonCancellable) {
+ job?.cancel()
+ try {
+ if (::session.isInitialized) session.cancel()
+ if (::scope.isInitialized) scope.cancel()
+ endpoint.cancel()
+ } catch (e: Throwable) {
+ _onError(e)
+ }
}
}
}
diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt
index 12529cc..5e990ee 100644
--- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt
+++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransport.kt
@@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
+import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
@@ -31,6 +32,7 @@ import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import kotlinx.io.Buffer
import kotlinx.io.IOException
@@ -225,7 +227,6 @@ public class StdioClientTransport @JvmOverloads public constructor(
}
override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) {
- @Suppress("SwallowedException")
try {
sendChannel.send(message)
} catch (e: CancellationException) {
@@ -241,8 +242,20 @@ public class StdioClientTransport @JvmOverloads public constructor(
}
override suspend fun closeResources() {
- scope.stopProcessing("Closed")
- scope.coroutineContext[Job]?.join() // Wait for all coroutines to complete
+ withContext(NonCancellable) {
+ scope.stopProcessing("Closed")
+ closeReadSources()
+ scope.coroutineContext[Job]?.join() // Wait for all coroutines to complete
+ }
+ }
+
+ private fun closeReadSources() {
+ runCatching { input.close() }
+ .onFailure { logger.debug(it) { "Error closing stdin source" } }
+ error?.let { source ->
+ runCatching { source.close() }
+ .onFailure { logger.debug(it) { "Error closing stderr source" } }
+ }
}
private fun sendOutboundMessage(message: JSONRPCMessage, sink: Sink, mainScope: CoroutineScope) {
@@ -264,6 +277,8 @@ public class StdioClientTransport @JvmOverloads public constructor(
private suspend fun handleJSONRPCMessage(msg: JSONRPCMessage) {
try {
_onMessage.invoke(msg)
+ } catch (e: CancellationException) {
+ throw e
} catch (e: Throwable) {
logger.error(e) { "Error processing message." }
runCatching { _onError.invoke(e) }
@@ -284,7 +299,7 @@ public class StdioClientTransport @JvmOverloads public constructor(
) {
val buffer = Buffer()
try {
- source.use { source ->
+ source.use {
while (isActive) {
val bytesRead = source.readAtMostTo(buffer, BUFFER_SIZE)
if (bytesRead == -1L) {
diff --git a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt
index 999e25a..6df352a 100644
--- a/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt
+++ b/kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt
@@ -21,8 +21,10 @@ import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.http.isSuccess
+import io.ktor.utils.io.charsets.TooLongLineException
import io.ktor.utils.io.readUTF8Line
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractClientTransport
+import io.modelcontextprotocol.kotlin.sdk.shared.TooLongFrameException
import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification
@@ -41,6 +43,10 @@ import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlin.io.encoding.Base64
+import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.pow
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@@ -49,11 +55,24 @@ import kotlin.time.Duration.Companion.seconds
private const val MCP_SESSION_ID_HEADER = "mcp-session-id"
private const val MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version"
private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID"
+private const val MCP_METHOD_HEADER = "Mcp-Method"
+private const val MCP_NAME_HEADER = "Mcp-Name"
+private const val MCP_BASE64_PREFIX = "=?base64?"
+private const val MCP_BASE64_SUFFIX = "?="
+
+/**
+ * Default maximum size, in characters, of a single inline SSE event assembled from a POST response.
+ *
+ * Mirrors the stdio transport's 16 MiB frame cap: a server that streams `data:` lines without ever
+ * terminating the event cannot grow the client's buffer without bound.
+ */
+private const val DEFAULT_MAX_INLINE_SSE_EVENT_SIZE: Int = 16 * 1024 * 1024
/**
* Represents an error from the Streamable HTTP transport.
*
* @property code HTTP status code associated with the error, or `null` if unavailable
+ * @param message detailed error description appended to the exception message
*/
public class StreamableHttpError(public val code: Int? = null, message: String? = null) :
Exception("Streamable HTTP error: $message")
@@ -65,21 +84,32 @@ private sealed interface ConnectResult {
}
/**
- * Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
- * It will connect to a server using HTTP POST for sending messages and HTTP GET with Server-Sent Events
- * for receiving messages.
+ * Client transport implementing the MCP Streamable HTTP transport specification.
*
- * @property sessionId session identifier assigned by the server after initialization, or `null` before connection
- * @property protocolVersion MCP protocol version negotiated with the server, or `null` before connection
+ * Sends messages via HTTP POST and receives messages via HTTP GET with Server-Sent Events.
+ * Supports automatic SSE reconnection with exponential backoff, stream resumption via the
+ * `Last-Event-ID` header, and explicit session termination.
+ *
+ * @param client Ktor HTTP client used for all requests
+ * @param url MCP endpoint URL
+ * @param reconnectionOptions reconnection backoff and retry-limit settings for the SSE stream
+ * @param maxInlineSseEventSize maximum size, in characters, of a single inline SSE event parsed from a
+ * POST response; a server that exceeds it (including by never terminating an event) fails the send
+ * with [io.modelcontextprotocol.kotlin.sdk.shared.TooLongFrameException]. Defaults to 16 MiB.
+ * @param requestBuilder builder applied to every outgoing HTTP request, e.g. for adding auth headers
*/
-@Suppress("TooManyFunctions")
public class StreamableHttpClientTransport(
private val client: HttpClient,
private val url: String,
private val reconnectionOptions: ReconnectionOptions = ReconnectionOptions(),
+ private val maxInlineSseEventSize: Int = DEFAULT_MAX_INLINE_SSE_EVENT_SIZE,
private val requestBuilder: HttpRequestBuilder.() -> Unit = {},
) : AbstractClientTransport() {
+ init {
+ require(maxInlineSseEventSize > 0) { "maxInlineSseEventSize must be greater than 0" }
+ }
+
@Deprecated(
"Use constructor with ReconnectionOptions",
replaceWith = ReplaceWith(
@@ -94,12 +124,20 @@ public class StreamableHttpClientTransport(
url: String,
reconnectionTime: Duration?,
requestBuilder: HttpRequestBuilder.() -> Unit = {},
- ) : this(client, url, ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds), requestBuilder)
+ ) : this(
+ client,
+ url,
+ ReconnectionOptions(initialReconnectionDelay = reconnectionTime ?: 1.seconds),
+ requestBuilder = requestBuilder,
+ )
override val logger: KLogger = KotlinLogging.logger {}
+ /** Session identifier assigned by the server after initialization, or `null` before connection. */
public var sessionId: String? = null
private set
+
+ /** MCP protocol version negotiated with the server, or `null` before connection. */
public var protocolVersion: String? = null
private var sseJob: Job? = null
@@ -121,7 +159,6 @@ public class StreamableHttpClientTransport(
/**
* Sends a single message with optional resumption support
*/
- @Suppress("ReturnCount", "CyclomaticComplexMethod", "LongMethod", "TooGenericExceptionCaught", "ThrowsCount")
override suspend fun performSend(message: JSONRPCMessage, options: TransportSendOptions?) {
logger.debug { "Client sending message via POST to $url: ${McpJson.encodeToString(message)}" }
@@ -138,6 +175,7 @@ public class StreamableHttpClientTransport(
val jsonBody = McpJson.encodeToString(message)
val response = client.post(url) {
applyCommonHeaders(this)
+ applyStandardPostHeaders(this, message)
headers.append(HttpHeaders.Accept, "${ContentType.Application.Json}, ${ContentType.Text.EventStream}")
contentType(ContentType.Application.Json)
setBody(jsonBody)
@@ -260,7 +298,6 @@ public class StreamableHttpClientTransport(
var attempt = 0
var needsDelay = initialServerRetryDelay != null
- @Suppress("LoopWithTooManyJumpStatements")
while (isActive) {
// Delay before (re)connection: skip only for first fresh SSE connection
if (needsDelay) {
@@ -296,7 +333,6 @@ public class StreamableHttpClientTransport(
}
}
- @Suppress("TooGenericExceptionCaught")
private suspend fun connectSse(lastEventId: String?): ConnectResult {
logger.debug { "Client attempting to start SSE session at url: $url" }
return try {
@@ -363,7 +399,36 @@ public class StreamableHttpClientTransport(
}
}
- @Suppress("TooGenericExceptionCaught")
+ private fun applyStandardPostHeaders(builder: HttpRequestBuilder, message: JSONRPCMessage) {
+ val (method, params) = when (message) {
+ is JSONRPCRequest -> message.method to message.params
+ is JSONRPCNotification -> message.method to message.params
+ else -> return
+ }
+
+ builder.headers {
+ append(MCP_METHOD_HEADER, method)
+
+ val paramsObject = params as? JsonObject ?: return@headers
+ val mcpName = paramsObject.stringValue("name") ?: paramsObject.stringValue("uri")
+ mcpName?.let { append(MCP_NAME_HEADER, it.encodeMcpHeaderValue()) }
+ }
+ }
+
+ private fun JsonObject.stringValue(key: String): String? =
+ (get(key) as? JsonPrimitive)?.takeIf { it.isString }?.content
+
+ @OptIn(ExperimentalEncodingApi::class)
+ private fun String.encodeMcpHeaderValue(): String {
+ val containsUnsafeCharacters = any { it != '\t' && it.code !in 0x20..0x7e }
+ val hasEdgeWhitespace = firstOrNull()?.isWhitespace() == true || lastOrNull()?.isWhitespace() == true
+ val matchesBase64Sentinel = startsWith(MCP_BASE64_PREFIX) && endsWith(MCP_BASE64_SUFFIX)
+
+ if (!containsUnsafeCharacters && !hasEdgeWhitespace && !matchesBase64Sentinel) return this
+
+ return "$MCP_BASE64_PREFIX${Base64.Default.encode(encodeToByteArray())}$MCP_BASE64_SUFFIX"
+ }
+
private suspend fun collectSse(
session: ClientSSESession,
replayMessageId: RequestId?,
@@ -408,7 +473,6 @@ public class StreamableHttpClientTransport(
return SseStreamResult(hasPrimingEvent, receivedResponse, localLastEventId, localServerRetryDelay)
}
- @Suppress("CyclomaticComplexMethod")
private suspend fun handleInlineSse(
response: HttpResponse,
replayMessageId: RequestId?,
@@ -455,9 +519,15 @@ public class StreamableHttpClientTransport(
}
}
- @Suppress("LoopWithTooManyJumpStatements")
while (!channel.isClosedForRead) {
- val line = channel.readUTF8Line() ?: break
+ // Bound each line so a server that streams a line without ever terminating it cannot
+ // exhaust client memory; readUTF8Line returns null at the end of the stream.
+ val line = try {
+ channel.readUTF8Line(maxInlineSseEventSize)
+ } catch (_: TooLongLineException) {
+ throw TooLongFrameException(maxInlineSseEventSize.toLong() + 1, maxInlineSseEventSize)
+ }
+ if (line == null) break
if (line.isEmpty()) {
dispatch(id = id, eventName = eventName, data = sb.toString())
// reset
@@ -471,7 +541,13 @@ public class StreamableHttpClientTransport(
line.startsWith("event:") -> eventName = line.substringAfter("event:").trim()
- line.startsWith("data:") -> sb.append(line.substringAfter("data:").trim())
+ line.startsWith("data:") -> {
+ sb.append(line.substringAfter("data:").trim())
+ // Cap an event assembled from many data: lines that never sees a terminating blank line.
+ if (sb.length > maxInlineSseEventSize) {
+ throw TooLongFrameException(sb.length.toLong(), maxInlineSseEventSize)
+ }
+ }
line.startsWith("retry:") -> line.substringAfter("retry:").trim().toLongOrNull()?.let {
localServerRetryDelay = it.milliseconds
diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt
new file mode 100644
index 0000000..e9379a7
--- /dev/null
+++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientAssertCapabilityTest.kt
@@ -0,0 +1,171 @@
+package io.modelcontextprotocol.kotlin.sdk.client
+
+import io.kotest.matchers.string.shouldContain
+import io.modelcontextprotocol.kotlin.sdk.shared.Transport
+import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions
+import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
+import io.modelcontextprotocol.kotlin.sdk.types.Implementation
+import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult
+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.Method
+import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
+import kotlinx.coroutines.test.runTest
+import kotlin.test.Test
+import kotlin.test.assertFailsWith
+
+/**
+ * Tests for the protected helper [Client.assertCapability].
+ *
+ * `assertCapability(capability, method)` is `protected`, so this suite uses
+ * a small subclass [TestClient] that re-exports it via [TestClient.exposedAssertCapability].
+ * Server capabilities are seeded by completing the initialization handshake through
+ * [CapabilitiesTransport], which replays a configurable [ServerCapabilities] in its
+ * `initialize` response.
+ */
+class ClientAssertCapabilityTest {
+
+ @Test
+ fun `assertCapability tasks throws when server has no tasks capability`() = runTest {
+ val client = newTestClient(serverCapabilities = ServerCapabilities())
+ val ex = assertFailsWith {
+ client.exposedAssertCapability("tasks", "tasks/list")
+ }
+ ex.message.orEmpty() shouldContain "Server does not support tasks"
+ }
+
+ @Test
+ fun `TasksGet throws when server has no tasks capability`() = runTest {
+ val client = newTestClient(serverCapabilities = ServerCapabilities())
+ val ex = assertFailsWith {
+ client.exposedAssertCapabilityForMethod(Method.Defined.TasksGet)
+ }
+ ex.message.orEmpty() shouldContain "Server does not support tasks"
+ }
+
+ @Test
+ fun `TasksGet does not throw when server declared tasks`() = runTest {
+ val client = newTestClient(
+ serverCapabilities = ServerCapabilities(tasks = ServerCapabilities.Tasks()),
+ )
+ client.exposedAssertCapabilityForMethod(Method.Defined.TasksGet)
+ }
+
+ @Test
+ fun `TasksList throws when server tasks list is null`() = runTest {
+ val client = newTestClient(
+ serverCapabilities = ServerCapabilities(tasks = ServerCapabilities.Tasks()),
+ )
+ val ex = assertFailsWith {
+ client.exposedAssertCapabilityForMethod(Method.Defined.TasksList)
+ }
+ ex.message.orEmpty() shouldContain "Server does not support listing tasks"
+ }
+
+ @Test
+ fun `TasksCancel throws when server tasks cancel is null`() = runTest {
+ val client = newTestClient(
+ serverCapabilities = ServerCapabilities(tasks = ServerCapabilities.Tasks()),
+ )
+ val ex = assertFailsWith {
+ client.exposedAssertCapabilityForMethod(Method.Defined.TasksCancel)
+ }
+ ex.message.orEmpty() shouldContain "Server does not support cancelling tasks"
+ }
+
+ @Test
+ fun `NotificationsTasksStatus throws when client has no tasks capability`() = runTest {
+ val client = newTestClient(clientCapabilities = ClientCapabilities())
+ val ex = assertFailsWith {
+ client.exposedAssertNotificationCapability(Method.Defined.NotificationsTasksStatus)
+ }
+ ex.message.orEmpty() shouldContain "Client does not support tasks"
+ }
+
+ @Test
+ fun `CompletionComplete does not throw when server declared completions`() = runTest {
+ val client = newTestClient(
+ serverCapabilities = ServerCapabilities(completions = ServerCapabilities.Completions),
+ )
+ client.exposedAssertCapabilityForMethod(Method.Defined.CompletionComplete)
+ }
+
+ @Test
+ fun `CompletionComplete throws when server has no completions capability`() = runTest {
+ val client = newTestClient(
+ serverCapabilities = ServerCapabilities(prompts = ServerCapabilities.Prompts()),
+ )
+ val ex = assertFailsWith {
+ client.exposedAssertCapabilityForMethod(Method.Defined.CompletionComplete)
+ }
+ ex.message.orEmpty() shouldContain "Server does not support completions"
+ }
+
+ private suspend fun newTestClient(
+ serverCapabilities: ServerCapabilities = ServerCapabilities(),
+ clientCapabilities: ClientCapabilities = ClientCapabilities(),
+ ): TestClient {
+ val client = TestClient(
+ Implementation("test-client", "1.0.0"),
+ ClientOptions(capabilities = clientCapabilities),
+ )
+ val transport = CapabilitiesTransport(serverCapabilities)
+ client.connect(transport)
+ return client
+ }
+
+ /**
+ * Test-only [Client] subclass that exposes the protected
+ * [Client.assertCapability], [Client.assertCapabilityForMethod], and
+ * [Client.assertNotificationCapability] helpers to the test code.
+ */
+ private class TestClient(clientInfo: Implementation, options: ClientOptions = ClientOptions()) :
+ Client(clientInfo, options) {
+ fun exposedAssertCapability(capability: String, method: String): Unit = assertCapability(capability, method)
+ fun exposedAssertCapabilityForMethod(method: Method): Unit = assertCapabilityForMethod(method)
+ fun exposedAssertNotificationCapability(method: Method): Unit = assertNotificationCapability(method)
+ }
+
+ /**
+ * Minimal in-memory [Transport] that responds to `initialize` with a configurable
+ * [ServerCapabilities]. Other JSON-RPC traffic (e.g. the `notifications/initialized`
+ * sent by [Client.connect]) is silently consumed.
+ */
+ private class CapabilitiesTransport(private val serverCapabilities: ServerCapabilities) : Transport {
+ private var onMessageBlock: (suspend (JSONRPCMessage) -> Unit)? = null
+ private var onCloseBlock: (() -> Unit)? = null
+
+ override suspend fun start() = Unit
+
+ override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) {
+ if (message is JSONRPCRequest && message.method == "initialize") {
+ onMessageBlock?.invoke(
+ JSONRPCResponse(
+ id = message.id,
+ result = InitializeResult(
+ protocolVersion = LATEST_PROTOCOL_VERSION,
+ capabilities = serverCapabilities,
+ serverInfo = Implementation("mock-server", "1.0.0"),
+ ),
+ ),
+ )
+ }
+ }
+
+ override suspend fun close() {
+ onCloseBlock?.invoke()
+ }
+
+ override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) {
+ onMessageBlock = block
+ }
+
+ override fun onClose(block: () -> Unit) {
+ onCloseBlock = block
+ }
+
+ override fun onError(block: (Throwable) -> Unit) = Unit
+ }
+}
diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt
index 3627b6c..14af546 100644
--- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt
+++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/MockSseClientEngine.kt
@@ -53,6 +53,14 @@ internal open class MockSseClientEngine(
job.cancel()
}
+ /**
+ * Closes the SSE byte channel without disposing the engine, simulating a server-side
+ * disconnect of the SSE stream.
+ */
+ fun disconnectSseStream() {
+ channel.close()
+ }
+
private fun responseContext(): CoroutineContext = dispatcher + Job()
private suspend fun respondWithSseEndpointEvent(data: HttpRequestData): HttpResponseData {
diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt
index 6cc7f6e..319ba2a 100644
--- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt
+++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/sse/SseClientTransportTest.kt
@@ -1,5 +1,6 @@
package io.modelcontextprotocol.kotlin.sdk.client.sse
+import io.kotest.assertions.nondeterministic.eventually
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import io.ktor.client.HttpClient
@@ -9,6 +10,7 @@ import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
+import kotlin.time.Duration.Companion.seconds
class SseClientTransportTest {
@@ -90,6 +92,31 @@ class SseClientTransportTest {
engine.close()
}
+ @Test
+ fun `onClose callback fires when the SSE stream is disconnected by the server`() = runTest {
+ // Given
+ val sseUrl = "http://example.com/api/mcp/sse"
+
+ // And
+ val engine = CapturingSseClientEngine(endpoint = "/messages?sessionId=abc")
+ val transport = sseTransport(sseUrl, engine)
+ var onCloseFired = false
+ transport.onClose { onCloseFired = true }
+
+ // When
+ transport.start()
+ engine.disconnectSseStream()
+
+ // Then
+ eventually(2.seconds) {
+ onCloseFired shouldBe true
+ }
+
+ // Cleanup
+ transport.close()
+ engine.close()
+ }
+
private fun sseTransport(sseUrl: String, engine: CapturingSseClientEngine) =
SseClientTransport(HttpClient(engine) { install(SSE) }, sseUrl)
diff --git a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt
index 67b2f07..d9aaf7a 100644
--- a/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt
+++ b/kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamableHttpClientTransportTest.kt
@@ -19,6 +19,7 @@ import io.ktor.http.headersOf
import io.ktor.utils.io.ByteReadChannel
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport
+import io.modelcontextprotocol.kotlin.sdk.shared.TooLongFrameException
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification
@@ -43,11 +44,15 @@ import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
import kotlin.test.assertTrue
+import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class StreamableHttpClientTransportTest {
- private fun createTransport(handler: MockRequestHandler): StreamableHttpClientTransport {
+ private fun createTransport(
+ maxInlineSseEventSize: Int = 16 * 1024 * 1024,
+ handler: MockRequestHandler,
+ ): StreamableHttpClientTransport {
val mockEngine = MockEngine(handler)
val httpClient = HttpClient(mockEngine) {
install(SSE) {
@@ -55,7 +60,11 @@ class StreamableHttpClientTransportTest {
}
}
- return StreamableHttpClientTransport(httpClient, url = "http://localhost:8080/mcp")
+ return StreamableHttpClientTransport(
+ httpClient,
+ url = "http://localhost:8080/mcp",
+ maxInlineSseEventSize = maxInlineSseEventSize,
+ )
}
private fun buildSseMessage(id: String, method: String, params: String): String = buildString {
@@ -237,6 +246,100 @@ class StreamableHttpClientTransportTest {
transport.close()
}
+ @Test
+ fun `should send MCP method and name headers from name`() = runTest {
+ val transport = createTransport { request ->
+ assertEquals("tools/call", request.headers["Mcp-Method"])
+ assertEquals("read_file", request.headers["Mcp-Name"])
+ respond(content = "", status = HttpStatusCode.Accepted)
+ }
+
+ transport.start()
+ transport.send(
+ JSONRPCRequest(
+ id = "test-id",
+ method = "tools/call",
+ params = buildJsonObject {
+ put("name", JsonPrimitive("read_file"))
+ put("arguments", buildJsonObject { put("path", JsonPrimitive("README.md")) })
+ },
+ ),
+ )
+ transport.close()
+ }
+
+ @Test
+ fun `should send MCP name header from URI`() = runTest {
+ val transport = createTransport { request ->
+ assertEquals("resources/read", request.headers["Mcp-Method"])
+ assertEquals("file:///README.md", request.headers["Mcp-Name"])
+ respond(content = "", status = HttpStatusCode.Accepted)
+ }
+
+ transport.start()
+ transport.send(
+ JSONRPCRequest(
+ id = "test-id",
+ method = "resources/read",
+ params = buildJsonObject { put("uri", JsonPrimitive("file:///README.md")) },
+ ),
+ )
+ transport.close()
+ }
+
+ @Test
+ fun `should encode MCP name header values when required`() = runTest {
+ val cases = listOf(
+ "Hello, 世界" to "=?base64?SGVsbG8sIOS4lueVjA==?=",
+ " padded " to "=?base64?IHBhZGRlZCA=?=",
+ "line1\nline2" to "=?base64?bGluZTEKbGluZTI=?=",
+ "=?base64?literal?=" to "=?base64?PT9iYXNlNjQ/bGl0ZXJhbD89?=",
+ )
+
+ cases.forEachIndexed { index, (name, expectedHeader) ->
+ val transport = createTransport { request ->
+ assertEquals(expectedHeader, request.headers["Mcp-Name"])
+ respond(content = "", status = HttpStatusCode.Accepted)
+ }
+
+ transport.start()
+ transport.send(
+ JSONRPCRequest(
+ id = "test-id-$index",
+ method = "tools/call",
+ params = buildJsonObject { put("name", JsonPrimitive(name)) },
+ ),
+ )
+ transport.close()
+ }
+ }
+
+ @Test
+ fun `should send MCP method header for notifications`() = runTest {
+ val transport = createTransport { request ->
+ assertEquals("notifications/tools/list_changed", request.headers["Mcp-Method"])
+ assertNull(request.headers["Mcp-Name"])
+ respond(content = "", status = HttpStatusCode.Accepted)
+ }
+
+ transport.start()
+ transport.send(JSONRPCNotification(method = "notifications/tools/list_changed"))
+ transport.close()
+ }
+
+ @Test
+ fun `should omit MCP standard headers for responses`() = runTest {
+ val transport = createTransport { request ->
+ assertNull(request.headers["Mcp-Method"])
+ assertNull(request.headers["Mcp-Name"])
+ respond(content = "", status = HttpStatusCode.Accepted)
+ }
+
+ transport.start()
+ transport.send(JSONRPCResponse(id = RequestId.StringId("test-id")))
+ transport.close()
+ }
+
// Engine doesn't support SSECapability: https://youtrack.jetbrains.com/issue/KTOR-8177/MockEngine-Add-SSE-support
@Ignore
@Test
@@ -478,6 +581,170 @@ class StreamableHttpClientTransportTest {
}
}
+ @Test
+ fun testSkipEmptySSE() = runTest {
+ val transport = createTransport { request ->
+ if (request.method == HttpMethod.Post) {
+ val sseContent = buildString {
+ // Event 1: empty data — must be skipped without error
+ appendLine("id: a")
+ appendLine("data:")
+ appendLine()
+ // Event 2: whitespace-only data — must be skipped without error
+ appendLine("id: b")
+ appendLine("data: \t ")
+ appendLine()
+ // Event 3: multi-line data lines that together form a valid JSON-RPC response
+ appendLine("id: c")
+ appendLine("event: message")
+ appendLine("data: {")
+ appendLine("""data: "jsonrpc":"2.0",""")
+ appendLine("""data: "id":"test-1",""")
+ appendLine("""data: "result":{""")
+ appendLine("""data: "protocolVersion":"2025-06-18",""")
+ appendLine("""data: "capabilities":{},""")
+ appendLine("""data: "serverInfo":{"name":"server","version":"1.0.0"}""")
+ appendLine("data: }")
+ appendLine("data: }")
+ appendLine()
+ }
+
+ respond(
+ content = ByteReadChannel(sseContent),
+ status = HttpStatusCode.OK,
+ headers = headersOf(
+ HttpHeaders.ContentType,
+ ContentType.Text.EventStream.toString(),
+ ),
+ )
+ } else {
+ respond("", HttpStatusCode.OK)
+ }
+ }
+
+ val receivedMessages = mutableListOf()
+ val messageReceived = CompletableDeferred()
+
+ transport.onMessage { message ->
+ receivedMessages.add(message)
+ if (!messageReceived.isCompleted) {
+ messageReceived.complete(Unit)
+ }
+ }
+
+ transport.start()
+
+ transport.send(
+ JSONRPCRequest(
+ id = "test-1",
+ method = "initialize",
+ params = buildJsonObject { },
+ ),
+ )
+
+ eventually {
+ messageReceived.await()
+ }
+
+ receivedMessages shouldHaveSize 1
+ val response = receivedMessages[0]
+ response.shouldBeInstanceOf()
+ response.id shouldBe RequestId.StringId("test-1")
+
+ transport.close()
+ }
+
+ @Test
+ fun testInlineSseRejectsEventExceedingMaxSize() = runTest {
+ // A malicious server streams an endless single event: many `data:` lines that accumulate
+ // past the cap and never send the blank-line terminator that would flush the buffer.
+ val maxInlineSseEventSize = 64
+ val transport = createTransport(maxInlineSseEventSize) { request ->
+ if (request.method == HttpMethod.Post) {
+ val sseContent = buildString {
+ appendLine("event: message")
+ // 20 lines × 16 chars = 320 chars of accumulated data, no terminating blank line.
+ repeat(20) { appendLine("data: ${"A".repeat(16)}") }
+ }
+ respond(
+ content = ByteReadChannel(sseContent),
+ status = HttpStatusCode.OK,
+ headers = headersOf(HttpHeaders.ContentType, ContentType.Text.EventStream.toString()),
+ )
+ } else {
+ respond("", HttpStatusCode.OK)
+ }
+ }
+
+ val receivedMessages = mutableListOf()
+ val receivedErrors = mutableListOf()
+ transport.onMessage { receivedMessages.add(it) }
+ transport.onError { receivedErrors.add(it) }
+ transport.start()
+
+ val error = assertFailsWith {
+ transport.send(JSONRPCRequest(id = "req-1", method = "test", params = buildJsonObject { }))
+ }
+
+ error.cause.shouldBeInstanceOf()
+ receivedErrors.filterIsInstance() shouldHaveSize 1
+ receivedMessages shouldHaveSize 0
+ transport.close()
+ }
+
+ @Test
+ fun testInlineSseEventExactlyAtMaxSizeIsAccepted() = runTest {
+ // An event whose assembled data length equals the cap must still be accepted and dispatched:
+ // the guard rejects only sizes strictly greater than the cap (parity with ReadBuffer).
+ val part1 = """{"jsonrpc":"2.0","""
+ val part2 = """"method":"notifications/tools/list_changed"}"""
+ val maxInlineSseEventSize = (part1 + part2).length
+
+ val transport = createTransport(maxInlineSseEventSize) { request ->
+ if (request.method == HttpMethod.Post) {
+ val sseContent = buildString {
+ appendLine("event: message")
+ appendLine("data: $part1")
+ appendLine("data: $part2")
+ appendLine()
+ }
+ respond(
+ content = ByteReadChannel(sseContent),
+ status = HttpStatusCode.OK,
+ headers = headersOf(HttpHeaders.ContentType, ContentType.Text.EventStream.toString()),
+ )
+ } else {
+ respond("", HttpStatusCode.OK)
+ }
+ }
+
+ val receivedMessages = mutableListOf()
+ val receivedErrors = mutableListOf()
+ val messageReceived = CompletableDeferred()
+ transport.onMessage {
+ receivedMessages.add(it)
+ if (!messageReceived.isCompleted) messageReceived.complete(Unit)
+ }
+ transport.onError { receivedErrors.add(it) }
+ transport.start()
+
+ transport.send(JSONRPCRequest(id = "req-1", method = "test", params = buildJsonObject { }))
+
+ eventually { messageReceived.await() }
+
+ receivedMessages shouldHaveSize 1
+ (receivedMessages[0] as JSONRPCNotification).method shouldBe "notifications/tools/list_changed"
+ receivedErrors shouldHaveSize 0
+ transport.close()
+ }
+
+ @Test
+ fun testNonPositiveMaxInlineSseEventSizeThrows() {
+ assertFailsWith {
+ createTransport(maxInlineSseEventSize = 0) { respond("", HttpStatusCode.OK) }
+ }
+ }
+
@Test
fun testInlineSSEInResponse() = runTest {
val transport = createTransport { request ->
@@ -643,7 +910,7 @@ class StreamableHttpClientTransportTest {
eventually {
while (receivedMessages.isEmpty()) {
- delay(10)
+ delay(10.milliseconds)
}
}
@@ -834,7 +1101,6 @@ class StreamableHttpClientTransportTest {
transport.close()
}
- @Suppress("DEPRECATION")
@Test
fun testDeprecatedConstructorStillWorks() = runTest {
val mockEngine = MockEngine { _ ->
diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt
deleted file mode 100644
index 4a04e81..0000000
--- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package io.modelcontextprotocol.kotlin.sdk.client
-
-import io.ktor.client.HttpClient
-import io.ktor.client.engine.apache5.Apache5
-import io.ktor.client.plugins.logging.LogLevel
-import io.ktor.client.plugins.logging.Logging
-import io.ktor.client.plugins.sse.SSE
-import org.junit.jupiter.api.AfterAll
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.TestInstance
-
-@TestInstance(TestInstance.Lifecycle.PER_CLASS)
-internal abstract class AbstractStreamableHttpClientTest {
-
- // start mokksy on random port
- protected val mockMcp: MockMcp = MockMcp(verbose = true)
-
- @AfterEach
- fun afterEach() {
- mockMcp.verifyNoUnexpectedRequests()
- }
-
- @AfterAll
- fun afterAll() {
- mockMcp.verifyNoUnmatchedStubs()
- mockMcp.close()
- }
-
- protected suspend fun connect(client: Client) {
- client.connect(
- StreamableHttpClientTransport(
- url = mockMcp.url,
- client = HttpClient(Apache5) {
- install(SSE)
- install(Logging) {
- level = LogLevel.ALL
- }
- },
- ),
- )
- }
-}
diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientSamplingValidationTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientSamplingValidationTest.kt
new file mode 100644
index 0000000..7775a08
--- /dev/null
+++ b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientSamplingValidationTest.kt
@@ -0,0 +1,98 @@
+package io.modelcontextprotocol.kotlin.sdk.client
+
+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.EmptyJsonObject
+import io.modelcontextprotocol.kotlin.sdk.types.McpException
+import io.modelcontextprotocol.kotlin.sdk.types.RPCError
+import io.modelcontextprotocol.kotlin.sdk.types.Role
+import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage
+import io.modelcontextprotocol.kotlin.sdk.types.TextContent
+import io.modelcontextprotocol.kotlin.sdk.types.Tool
+import io.modelcontextprotocol.kotlin.sdk.types.ToolChoice
+import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
+import kotlinx.serialization.json.buildJsonObject
+import org.junit.jupiter.api.assertDoesNotThrow
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class ClientSamplingValidationTest {
+
+ private val dummyTool = Tool(
+ name = "t",
+ inputSchema = ToolSchema(properties = buildJsonObject { }, required = emptyList()),
+ )
+
+ private val minimalMessages = listOf(SamplingMessage(Role.User, TextContent("hi")))
+
+ private val noToolsCaps = ClientCapabilities(sampling = ClientCapabilities.sampling)
+ private val withToolsCaps = ClientCapabilities(
+ sampling = ClientCapabilities.Sampling(tools = EmptyJsonObject),
+ )
+
+ @Test
+ fun `request without tools or toolChoice is always accepted`() {
+ val request = CreateMessageRequest(
+ CreateMessageRequestParams(maxTokens = 10, messages = minimalMessages),
+ )
+ assertDoesNotThrow { validateSamplingToolsCapability(request, noToolsCaps) }
+ }
+
+ @Test
+ fun `tools without sampling tools capability throws InvalidParams`() {
+ val request = CreateMessageRequest(
+ CreateMessageRequestParams(
+ maxTokens = 10,
+ messages = minimalMessages,
+ tools = listOf(dummyTool),
+ ),
+ )
+ val exception = assertFailsWith {
+ validateSamplingToolsCapability(request, noToolsCaps)
+ }
+ assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code)
+ check(exception.message?.contains("tools") == true)
+ }
+
+ @Test
+ fun `toolChoice without sampling tools capability throws InvalidParams`() {
+ val request = CreateMessageRequest(
+ CreateMessageRequestParams(
+ maxTokens = 10,
+ messages = minimalMessages,
+ toolChoice = ToolChoice(mode = ToolChoice.Mode.Required),
+ ),
+ )
+ val exception = assertFailsWith {
+ validateSamplingToolsCapability(request, noToolsCaps)
+ }
+ assertEquals(RPCError.ErrorCode.INVALID_PARAMS, exception.code)
+ check(exception.message?.contains("toolChoice") == true)
+ }
+
+ @Test
+ fun `tools with sampling tools capability is accepted`() {
+ val request = CreateMessageRequest(
+ CreateMessageRequestParams(
+ maxTokens = 10,
+ messages = minimalMessages,
+ tools = listOf(dummyTool),
+ ),
+ )
+ assertDoesNotThrow { validateSamplingToolsCapability(request, withToolsCaps) }
+ }
+
+ @Test
+ fun `toolChoice with sampling tools capability is accepted`() {
+ val request = CreateMessageRequest(
+ CreateMessageRequestParams(
+ maxTokens = 10,
+ messages = minimalMessages,
+ toolChoice = ToolChoice(mode = ToolChoice.Mode.Auto),
+ ),
+ )
+ assertDoesNotThrow { validateSamplingToolsCapability(request, withToolsCaps) }
+ }
+}
diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt
deleted file mode 100644
index fb71904..0000000
--- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockMcp.kt
+++ /dev/null
@@ -1,241 +0,0 @@
-package io.modelcontextprotocol.kotlin.sdk.client
-
-import dev.mokksy.mokksy.BuildingStep
-import dev.mokksy.mokksy.Mokksy
-import dev.mokksy.mokksy.StubConfiguration
-import dev.mokksy.mokksy.shutdown
-import dev.mokksy.mokksy.start
-import io.ktor.http.ContentType
-import io.ktor.http.HttpMethod
-import io.ktor.http.HttpStatusCode
-import io.ktor.sse.ServerSentEvent
-import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest
-import io.modelcontextprotocol.kotlin.sdk.types.RequestId
-import kotlinx.coroutines.flow.Flow
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonObject
-import kotlinx.serialization.json.JsonPrimitive
-import kotlinx.serialization.json.buildJsonObject
-import kotlinx.serialization.json.contentOrNull
-import kotlinx.serialization.json.jsonObject
-import kotlinx.serialization.json.jsonPrimitive
-import kotlinx.serialization.json.putJsonObject
-
-const val MCP_SESSION_ID_HEADER = "MCP-Session-Id"
-
-internal class MockMcp(verbose: Boolean = false) : AutoCloseable {
-
- private val mokksy: Mokksy = Mokksy(verbose = verbose).apply {
- start()
- }
-
- fun verifyNoUnexpectedRequests() {
- mokksy.verifyNoUnexpectedRequests()
- }
-
- fun verifyNoUnmatchedStubs() {
- mokksy.verifyNoUnmatchedStubs()
- }
-
- val url = "${mokksy.baseUrl()}/mcp"
-
- @Suppress("LongParameterList")
- fun onInitialize(
- clientName: String? = null,
- sessionId: String,
- protocolVersion: String = "2025-03-26",
- serverName: String = "Mock MCP Server",
- serverVersion: String = "1.0.0",
- capabilities: JsonObject = buildJsonObject {
- putJsonObject("tools") {
- put("listChanged", JsonPrimitive(false))
- }
- },
- ) {
- val predicates = if (clientName != null) {
- arrayOf<(JSONRPCRequest?) -> Boolean>({
- it?.params?.jsonObject
- ?.get("clientInfo")?.jsonObject
- ?.get("name")?.jsonPrimitive
- ?.contentOrNull == clientName
- })
- } else {
- emptyArray()
- }
-
- handleWithResult(
- jsonRpcMethod = "initialize",
- sessionId = sessionId,
- bodyPredicates = predicates,
- // language=json
- result = """
- {
- "capabilities": $capabilities,
- "protocolVersion": "$protocolVersion",
- "serverInfo": {
- "name": "$serverName",
- "version": "$serverVersion"
- },
- "_meta": {
- "foo": "bar"
- }
- }
- """.trimIndent(),
- )
- }
-
- fun onJSONRPCRequest(
- httpMethod: HttpMethod = HttpMethod.Post,
- jsonRpcMethod: String,
- expectedSessionId: String? = null,
- vararg bodyPredicates: (JSONRPCRequest) -> Boolean,
- ): BuildingStep = mokksy.method(
- configuration = StubConfiguration(removeAfterMatch = true),
- httpMethod = httpMethod,
- requestType = JSONRPCRequest::class,
- ) {
- path("/mcp")
- expectedSessionId?.let {
- containsHeader(MCP_SESSION_ID_HEADER, it)
- }
- bodyMatchesPredicate(
- description = "JSON-RPC version is '2.0'",
- predicate =
- {
- it!!.jsonrpc == "2.0"
- },
- )
- bodyMatchesPredicate(
- description = "JSON-RPC Method should be '$jsonRpcMethod'",
- predicate =
- {
- it!!.method == jsonRpcMethod
- },
- )
- bodyPredicates.forEach { predicate ->
- bodyMatchesPredicate(predicate = { predicate.invoke(it!!) })
- }
- }
-
- @Suppress("LongParameterList")
- fun handleWithResult(
- httpMethod: HttpMethod = HttpMethod.Post,
- jsonRpcMethod: String,
- expectedSessionId: String? = null,
- sessionId: String,
- contentType: ContentType = ContentType.Application.Json,
- statusCode: HttpStatusCode = HttpStatusCode.OK,
- vararg bodyPredicates: (JSONRPCRequest) -> Boolean,
- result: () -> JsonObject,
- ) {
- onJSONRPCRequest(
- httpMethod = httpMethod,
- jsonRpcMethod = jsonRpcMethod,
- expectedSessionId = expectedSessionId,
- bodyPredicates = bodyPredicates,
- ) respondsWith {
- val requestId = when (val id = request.body().id) {
- is RequestId.NumberId -> id.value.toString()
- is RequestId.StringId -> "\"${id.value}\""
- }
- val resultObject = result.invoke()
- // language=json
- body = """
- {
- "jsonrpc": "2.0",
- "id": $requestId,
- "result": $resultObject
- }
- """.trimIndent()
- this.contentType = contentType
- headers += MCP_SESSION_ID_HEADER to sessionId
- httpStatus = statusCode
- }
- }
-
- @Suppress("LongParameterList")
- fun handleWithResult(
- httpMethod: HttpMethod = HttpMethod.Post,
- jsonRpcMethod: String,
- expectedSessionId: String? = null,
- sessionId: String,
- contentType: ContentType = ContentType.Application.Json,
- statusCode: HttpStatusCode = HttpStatusCode.OK,
- vararg bodyPredicates: (JSONRPCRequest) -> Boolean,
- result: String,
- ) {
- handleWithResult(
- httpMethod = httpMethod,
- jsonRpcMethod = jsonRpcMethod,
- expectedSessionId = expectedSessionId,
- sessionId = sessionId,
- contentType = contentType,
- statusCode = statusCode,
- bodyPredicates = bodyPredicates,
- result = {
- Json.parseToJsonElement(result).jsonObject
- },
- )
- }
-
- @Suppress("LongParameterList")
- fun handleJSONRPCRequest(
- httpMethod: HttpMethod = HttpMethod.Post,
- jsonRpcMethod: String,
- expectedSessionId: String? = null,
- sessionId: String,
- contentType: ContentType = ContentType.Application.Json,
- statusCode: HttpStatusCode = HttpStatusCode.OK,
- vararg bodyPredicates: (JSONRPCRequest?) -> Boolean,
- bodyBuilder: () -> String = { "" },
- ) {
- onJSONRPCRequest(
- httpMethod = httpMethod,
- jsonRpcMethod = jsonRpcMethod,
- expectedSessionId = expectedSessionId,
- bodyPredicates = bodyPredicates,
- ) respondsWith {
- body = bodyBuilder.invoke()
- this.contentType = contentType
- headers += MCP_SESSION_ID_HEADER to sessionId
- httpStatus = statusCode
- }
- }
-
- fun onSubscribe(httpMethod: HttpMethod = HttpMethod.Post, sessionId: String): BuildingStep = mokksy.method(
- httpMethod = httpMethod,
- name = "MCP GETs",
- requestType = Any::class,
- ) {
- path("/mcp")
- containsHeader(MCP_SESSION_ID_HEADER, sessionId)
- containsHeader("Accept", "application/json,text/event-stream")
- containsHeader("Cache-Control", "no-store")
- }
-
- fun handleSubscribeWithGet(sessionId: String, block: () -> Flow) {
- onSubscribe(
- httpMethod = HttpMethod.Get,
- sessionId = sessionId,
- ) respondsWithSseStream {
- headers += MCP_SESSION_ID_HEADER to sessionId
- this.flow = block.invoke()
- }
- }
-
- fun mockUnsubscribeRequest(sessionId: String) {
- mokksy.delete(
- configuration = StubConfiguration(removeAfterMatch = true),
- requestType = JSONRPCRequest::class,
- ) {
- path("/mcp")
- containsHeader(MCP_SESSION_ID_HEADER, sessionId)
- } respondsWith {
- body = null
- }
- }
-
- override fun close() {
- mokksy.shutdown()
- }
-}
diff --git a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt b/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt
deleted file mode 100644
index a1fe89e..0000000
--- a/kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTest.kt
+++ /dev/null
@@ -1,293 +0,0 @@
-package io.modelcontextprotocol.kotlin.sdk.client
-
-import io.kotest.matchers.collections.shouldContain
-import io.kotest.matchers.shouldBe
-import io.ktor.http.ContentType
-import io.ktor.http.HttpMethod
-import io.ktor.http.HttpStatusCode
-import io.ktor.sse.ServerSentEvent
-import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
-import io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject
-import io.modelcontextprotocol.kotlin.sdk.types.Implementation
-import io.modelcontextprotocol.kotlin.sdk.types.Tool
-import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.runBlocking
-import kotlinx.serialization.json.buildJsonObject
-import kotlinx.serialization.json.put
-import kotlinx.serialization.json.putJsonObject
-import org.junit.jupiter.api.TestInstance
-import kotlin.test.Test
-import kotlin.time.Duration.Companion.milliseconds
-import kotlin.time.Duration.Companion.seconds
-import kotlin.uuid.ExperimentalUuidApi
-import kotlin.uuid.Uuid
-
-@OptIn(ExperimentalUuidApi::class)
-@TestInstance(TestInstance.Lifecycle.PER_CLASS)
-internal class StreamableHttpClientTest : AbstractStreamableHttpClientTest() {
-
- @Test
- fun `Should skip empty SSE`(): Unit = runBlocking {
- val client = Client(
- clientInfo = Implementation(
- name = "client1",
- version = "1.0.0",
- ),
- options = ClientOptions(
- capabilities = ClientCapabilities(),
- ),
- )
- val sessionId = Uuid.random().toString()
-
- mockMcp.onJSONRPCRequest(
- httpMethod = HttpMethod.Post,
- jsonRpcMethod = "initialize",
- ).respondsWithStream {
- headers += MCP_SESSION_ID_HEADER to sessionId
- flow = flowOf(
- "id: ${Uuid.random()}\n",
- "data:\n", // empty data
- "\n",
- "id: ${Uuid.random()}\n",
- "data: \t \n", // tabs and spaces
- "\n",
- "id: ${Uuid.random()}\n",
- "event: message\n",
- // multiline data
- "data: {\n",
- "data: \"result\":{\n" +
- "data: \"protocolVersion\":\"2025-06-18\",\n" +
- "data: \"capabilities\":{},\n" +
- "data: \"serverInfo\":{\"name\":\"simple-streamable-http-server\",\"version\":\"1.0.0\"}\n" +
- "data: },\n" +
- "data: \"jsonrpc\":\"2.0\",\n" +
- "data: \"id\":\"7ce065b0678f49e5b04ce5a0fcc7d518\"\n" +
- "data: }\n",
- "\n",
- )
- }
-
- mockMcp.handleJSONRPCRequest(
- jsonRpcMethod = "notifications/initialized",
- expectedSessionId = sessionId,
- sessionId = sessionId,
- statusCode = HttpStatusCode.Accepted,
- )
-
- mockMcp.handleSubscribeWithGet(sessionId) {
- emptyFlow()
- }
-
- connect(client)
- }
-
- @Test
- fun `test streamableHttpClient`() = runBlocking {
- val client = Client(
- clientInfo = Implementation(
- name = "client1",
- version = "1.0.0",
- ),
- options = ClientOptions(
- capabilities = ClientCapabilities(),
- ),
- )
-
- val sessionId = Uuid.random().toString()
-
- mockMcp.onInitialize(
- clientName = "client1",
- sessionId = sessionId,
- )
-
- mockMcp.handleJSONRPCRequest(
- jsonRpcMethod = "notifications/initialized",
- expectedSessionId = sessionId,
- sessionId = sessionId,
- statusCode = HttpStatusCode.Accepted,
- )
-
- mockMcp.handleSubscribeWithGet(sessionId) {
- flow {
- delay(500.milliseconds)
- emit(
- ServerSentEvent(
- event = "message",
- id = "1",
- data = @Suppress("MaxLineLength")
- //language=json
- """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""",
- ),
- )
- delay(200.milliseconds)
- emit(
- ServerSentEvent(
- data = @Suppress("MaxLineLength")
- //language=json
- """{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"upload-123","progress":50,"total":100}}""",
- ),
- )
- }
- }
-
- // TODO: how to get notifications via Client API?
-
- mockMcp.handleWithResult(
- jsonRpcMethod = "tools/list",
- sessionId = sessionId,
- // language=json
- result = """
- {
- "tools": [
- {
- "name": "get_weather",
- "title": "Weather Information Provider",
- "description": "Get current weather information for a location",
- "inputSchema": {
- "type": "object",
- "properties": {
- "location": {
- "type": "string",
- "description": "City name or zip code"
- }
- },
- "required": ["location"]
- },
- "outputSchema": {
- "type": "object",
- "properties": {
- "temperature": {
- "type": "number",
- "description": "Temperature, Celsius"
- }
- },
- "required": ["temperature"]
- },
- "_meta": {}
- }
- ]
- }
- """.trimIndent(),
- )
-
- connect(client)
-
- val listToolsResult = client.listTools()
-
- listToolsResult.tools shouldContain Tool(
- name = "get_weather",
- title = "Weather Information Provider",
- description = "Get current weather information for a location",
- inputSchema = ToolSchema(
- properties = buildJsonObject {
- putJsonObject("location") {
- put("type", "string")
- put("description", "City name or zip code")
- }
- },
- required = listOf("location"),
- ),
- outputSchema = ToolSchema(
- properties = buildJsonObject {
- putJsonObject("temperature") {
- put("type", "number")
- put("description", "Temperature, Celsius")
- }
- },
- required = listOf("temperature"),
- ),
- annotations = null,
- meta = EmptyJsonObject,
- )
-
- client.close()
- }
-
- @Test
- fun `terminateSession sends DELETE request`() = runBlocking {
- val client = Client(
- clientInfo = Implementation(name = "client1", version = "1.0.0"),
- options = ClientOptions(capabilities = ClientCapabilities()),
- )
- val sessionId = Uuid.random().toString()
-
- mockMcp.onInitialize(clientName = "client1", sessionId = sessionId)
- mockMcp.handleJSONRPCRequest(
- jsonRpcMethod = "notifications/initialized",
- expectedSessionId = sessionId,
- sessionId = sessionId,
- statusCode = HttpStatusCode.Accepted,
- )
- mockMcp.handleSubscribeWithGet(sessionId) { emptyFlow() }
-
- connect(client)
-
- mockMcp.mockUnsubscribeRequest(sessionId = sessionId)
- (client.transport as StreamableHttpClientTransport).terminateSession()
- (client.transport as StreamableHttpClientTransport).sessionId shouldBe null
-
- client.close()
- }
-
- @Test
- fun `handle MethodNotAllowed`() = runBlocking {
- checkSupportNonStreamingResponse(
- ContentType.Text.EventStream,
- HttpStatusCode.MethodNotAllowed,
- )
- }
-
- @Test
- fun `handle non-streaming response`() = runBlocking {
- checkSupportNonStreamingResponse(
- ContentType.Application.Json,
- HttpStatusCode.OK,
- )
- }
-
- private suspend fun checkSupportNonStreamingResponse(contentType: ContentType, statusCode: HttpStatusCode) {
- val sessionId = "SID_${Uuid.random().toHexString()}"
- val clientName = "client-${Uuid.random().toHexString()}"
- val client = Client(
- clientInfo = Implementation(name = clientName, version = "1.0.0"),
- options = ClientOptions(
- capabilities = ClientCapabilities(),
- ),
- )
-
- mockMcp.onInitialize(clientName = clientName, sessionId = sessionId)
-
- mockMcp.handleJSONRPCRequest(
- jsonRpcMethod = "notifications/initialized",
- expectedSessionId = sessionId,
- sessionId = sessionId,
- statusCode = HttpStatusCode.Accepted,
- )
-
- mockMcp.onSubscribe(
- httpMethod = HttpMethod.Get,
- sessionId = sessionId,
- ) respondsWith {
- headers += MCP_SESSION_ID_HEADER to sessionId
- body = null
- httpStatus = statusCode
- this.contentType = contentType
- }
-
- mockMcp.handleWithResult(jsonRpcMethod = "ping", sessionId = sessionId) {
- buildJsonObject {}
- }
-
- connect(client)
-
- delay(1.seconds)
-
- client.ping() // connection is still alive
-
- client.close()
- }
-}
diff --git a/kotlin-sdk-core/.openapi-generator-ignore b/kotlin-sdk-core/.openapi-generator-ignore
deleted file mode 100644
index dbdf930..0000000
--- a/kotlin-sdk-core/.openapi-generator-ignore
+++ /dev/null
@@ -1,29 +0,0 @@
-# OpenAPI Generator Ignore
-# Generated by openapi-generator https://github.com/openapitools/openapi-generator
-
-# Use this file to prevent files from being overwritten by the generator.
-# The patterns follow closely to .gitignore or .dockerignore.
-
-# As an example, the C# client generator defines ApiClient.cs.
-# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
-**/auth/**
-**/infrastructure/**
-!**/Base64ByteArray.kt
-!**/Bytes.kt
-
-
-## Exclude Models which are causing problems
-**/JSONRPCResponse.kt
-**/Result.kt
-**/ListToolsResult.kt
-**/InitializeRequest.kt
-**/InitializeResult.kt
-**/ServerCapabilities.kt
-**/InitializeRequestParams.kt
-**/CreateMessageRequest.kt
-**/CreateMessageRequestParams.kt
-**/ClientCapabilities.kt
-**/Tool.kt
-**/ToolInputSchema.kt
-**/ToolOutputSchema.kt
-
diff --git a/kotlin-sdk-core/api/kotlin-sdk-core.api b/kotlin-sdk-core/api/kotlin-sdk-core.api
index d4f88ba..b3e88d8 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;
@@ -79,19 +81,23 @@ public abstract class io/modelcontextprotocol/kotlin/sdk/shared/Protocol {
public final fun setFallbackRequestHandler (Lkotlin/jvm/functions/Function3;)V
public final fun setNotificationHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function1;)V
public final fun setRequestHandlerInternal (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function3;)V
+ protected fun wrapRequestHandler (Lio/modelcontextprotocol/kotlin/sdk/types/Method;Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function3;
}
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
}
@@ -106,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 {
@@ -124,6 +138,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/shared/RequestOptions : io
public fun toString ()Ljava/lang/String;
}
+public final class io/modelcontextprotocol/kotlin/sdk/shared/TooLongFrameException : java/io/IOException {
+ public fun (JI)V
+ public final fun getFrameSize ()J
+ public final fun getMaxFrameSize ()I
+}
+
public abstract interface class io/modelcontextprotocol/kotlin/sdk/shared/Transport {
public abstract fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun onClose (Lkotlin/jvm/functions/Function0;)V
@@ -370,6 +390,19 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/BooleanSchema$Compan
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
+public final class io/modelcontextprotocol/kotlin/sdk/types/CacheScope : java/lang/Enum {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope$Companion;
+ public static final field Private Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope;
+ public static final field Public Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
+ public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope;
+ public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/CacheScope$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequest$Companion;
public fun (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;)V
@@ -383,6 +416,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequest : io
public final fun getName ()Ljava/lang/String;
public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;
public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams;
+ public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
@@ -407,24 +441,26 @@ 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
}
public final class io/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams$Companion;
- public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lkotlinx/serialization/json/JsonObject;
- public final fun component3-VI-3G7E ()Lkotlinx/serialization/json/JsonObject;
- public final fun copy-EDEVuBg (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;
- public static synthetic fun copy-EDEVuBg$default (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;
+ public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;
+ public final fun component4-VI-3G7E ()Lkotlinx/serialization/json/JsonObject;
+ public final fun copy-6R815Ik (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;
+ public static synthetic fun copy-6R815Ik$default (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolRequestParams;
public fun equals (Ljava/lang/Object;)Z
public final fun getArguments ()Lkotlinx/serialization/json/JsonObject;
public fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject;
public final fun getName ()Ljava/lang/String;
+ public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
@@ -603,22 +639,32 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CancelledNotificatio
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
+public final class io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesKt {
+ public static final fun getSupportsUrl (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;)Z
+}
+
public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Companion;
public fun ()V
- public fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V
- public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public final fun component1 ()Lkotlinx/serialization/json/JsonObject;
+ public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V
+ public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V
+ public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;
public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;
- public final fun component3 ()Lkotlinx/serialization/json/JsonObject;
- public final fun component4 ()Lkotlinx/serialization/json/JsonObject;
- public final fun copy (Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;
- public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;
+ public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;
+ public final fun component4 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;
+ public final fun component5 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun component6 ()Ljava/util/Map;
+ public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;
public fun equals (Ljava/lang/Object;)Z
- public final fun getElicitation ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getElicitation ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;
public final fun getExperimental ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getExtensions ()Ljava/util/Map;
public final fun getRoots ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Roots;
- public final fun getSampling ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getSampling ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;
+ public final fun getTasks ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
@@ -635,8 +681,39 @@ public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapa
}
public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Companion {
- public final fun getElicitation ()Lkotlinx/serialization/json/JsonObject;
- public final fun getSampling ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getElicitation ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;
+ public final fun getSampling ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation$Companion;
+ public fun ()V
+ public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V
+ public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun component2 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getForm ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getUrl ()Lkotlinx/serialization/json/JsonObject;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
@@ -669,17 +746,169 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$R
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
+public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling$Companion;
+ public fun ()V
+ public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V
+ public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun component2 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getContext ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getTools ()Lkotlinx/serialization/json/JsonObject;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Companion;
+ public fun ()V
+ public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;)V
+ public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun component2 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;
+ public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getCancel ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getList ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getRequests ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Companion;
+ public fun ()V
+ public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;)V
+ public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;
+ public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;
+ public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getElicitation ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;
+ public final fun getSampling ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation$Companion;
+ public fun ()V
+ public fun (Lkotlinx/serialization/json/JsonObject;)V
+ public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getCreate ()Lkotlinx/serialization/json/JsonObject;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Elicitation$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling$Companion;
+ public fun ()V
+ public fun (Lkotlinx/serialization/json/JsonObject;)V
+ public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getCreateMessage ()Lkotlinx/serialization/json/JsonObject;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Tasks$Requests$Sampling$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
public final class io/modelcontextprotocol/kotlin/sdk/types/ClientCapabilitiesBuilder {
public fun ()V
public final fun build ()Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;
- public final fun elicitation (Lkotlin/jvm/functions/Function1;)V
- public final fun elicitation (Lkotlinx/serialization/json/JsonObject;)V
+ public final fun elicitation (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Elicitation;)V
public final fun experimental (Lkotlin/jvm/functions/Function1;)V
public final fun experimental (Lkotlinx/serialization/json/JsonObject;)V
+ public final fun extensions (Ljava/util/Map;)V
public final fun roots (Ljava/lang/Boolean;)V
public static synthetic fun roots$default (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilitiesBuilder;Ljava/lang/Boolean;ILjava/lang/Object;)V
- public final fun sampling (Lkotlin/jvm/functions/Function1;)V
- public final fun sampling (Lkotlinx/serialization/json/JsonObject;)V
+ public final fun sampling (Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities$Sampling;)V
}
public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/ClientNotification : io/modelcontextprotocol/kotlin/sdk/types/Notification {
@@ -751,7 +980,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
@@ -931,6 +1160,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ContentTypes : java/
public static final field IMAGE Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes;
public static final field RESOURCE_LINK Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes;
public static final field TEXT Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes;
+ public static final field TOOL_RESULT Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes;
+ public static final field TOOL_USE Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public final fun getValue ()Ljava/lang/String;
public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes;
@@ -959,6 +1190,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest
public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams;
public final fun getStopSequences ()Ljava/util/List;
public final fun getSystemPrompt ()Ljava/lang/String;
+ public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;
public final fun getTemperature ()Ljava/lang/Double;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
@@ -982,7 +1214,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;
@@ -1003,9 +1235,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest
public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams$Companion;
- public synthetic fun (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public synthetic fun (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public synthetic fun (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public synthetic fun (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()I
+ public final fun component10 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;
+ public final fun component11 ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;
+ public final fun component12-VI-3G7E ()Lkotlinx/serialization/json/JsonObject;
public final fun component2 ()Ljava/util/List;
public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;
public final fun component4 ()Ljava/lang/String;
@@ -1013,9 +1248,9 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest
public final fun component6 ()Ljava/lang/Double;
public final fun component7 ()Ljava/util/List;
public final fun component8 ()Lkotlinx/serialization/json/JsonObject;
- public final fun component9-VI-3G7E ()Lkotlinx/serialization/json/JsonObject;
- public final fun copy-qtvSJzw (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;
- public static synthetic fun copy-qtvSJzw$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;
+ public final fun component9 ()Ljava/util/List;
+ public final fun copy-7OuPiQU (ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;
+ public static synthetic fun copy-7OuPiQU$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;ILjava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;Ljava/lang/String;Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;Ljava/lang/Double;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequestParams;
public fun equals (Ljava/lang/Object;)Z
public final fun getIncludeContext ()Lio/modelcontextprotocol/kotlin/sdk/types/IncludeContext;
public final fun getMaxTokens ()I
@@ -1025,7 +1260,10 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest
public final fun getModelPreferences ()Lio/modelcontextprotocol/kotlin/sdk/types/ModelPreferences;
public final fun getStopSequences ()Ljava/util/List;
public final fun getSystemPrompt ()Ljava/lang/String;
+ public final fun getTask ()Lio/modelcontextprotocol/kotlin/sdk/types/TaskMetadata;
public final fun getTemperature ()Ljava/lang/Double;
+ public final fun getToolChoice ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;
+ public final fun getTools ()Ljava/util/List;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
@@ -1047,17 +1285,19 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageRequest
public final class io/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult : io/modelcontextprotocol/kotlin/sdk/types/ClientResult {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult$Companion;
- public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Role;
- public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;
+ public final fun component2 ()Ljava/util/List;
public final fun component3 ()Ljava/lang/String;
public final fun component4-6olV-UY ()Ljava/lang/String;
public final fun component5 ()Lkotlinx/serialization/json/JsonObject;
- public final fun copy-ctVX1tw (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult;
- public static synthetic fun copy-ctVX1tw$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult;Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult;
+ public final fun copy-ctVX1tw (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult;
+ public static synthetic fun copy-ctVX1tw$default (Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult;Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CreateMessageResult;
public fun equals (Ljava/lang/Object;)Z
- public final fun getContent ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;
+ public final fun getContent ()Ljava/util/List;
public fun getMeta ()Lkotlinx/serialization/json/JsonObject;
public final fun getModel ()Ljava/lang/String;
public final fun getRole ()Lio/modelcontextprotocol/kotlin/sdk/types/Role;
@@ -1168,6 +1408,104 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/CustomRequest$Compan
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
+public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest$Companion;
+ public fun (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;)V
+ public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;
+ public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest;Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getMeta--Khlwt8 ()Lkotlinx/serialization/json/JsonObject;
+ public fun getMethod ()Lio/modelcontextprotocol/kotlin/sdk/types/Method;
+ public fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;
+ public synthetic fun getParams ()Lio/modelcontextprotocol/kotlin/sdk/types/RequestParams;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequest$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams : io/modelcontextprotocol/kotlin/sdk/types/RequestParams {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams$Companion;
+ public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1--Khlwt8 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun copy-hVVZdjo (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;
+ public static synthetic fun copy-hVVZdjo$default (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;
+ public fun equals (Ljava/lang/Object;)Z
+ public fun getMeta--Khlwt8 ()Lkotlinx/serialization/json/JsonObject;
+ public synthetic fun getMeta-VI-3G7E ()Lkotlinx/serialization/json/JsonObject;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverRequestParams$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverResult : io/modelcontextprotocol/kotlin/sdk/types/ServerResult {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult$Companion;
+ public fun (Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Ljava/lang/String;Ljava/lang/String;JLio/modelcontextprotocol/kotlin/sdk/types/CacheScope;Lkotlinx/serialization/json/JsonObject;)V
+ public synthetic fun (Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Ljava/lang/String;Ljava/lang/String;JLio/modelcontextprotocol/kotlin/sdk/types/CacheScope;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/util/List;
+ public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;
+ public final fun component3 ()Ljava/lang/String;
+ public final fun component4 ()Ljava/lang/String;
+ public final fun component5 ()J
+ public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope;
+ public final fun component7 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun copy (Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Ljava/lang/String;Ljava/lang/String;JLio/modelcontextprotocol/kotlin/sdk/types/CacheScope;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Ljava/lang/String;Ljava/lang/String;JLio/modelcontextprotocol/kotlin/sdk/types/CacheScope;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getCacheScope ()Lio/modelcontextprotocol/kotlin/sdk/types/CacheScope;
+ public final fun getCapabilities ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;
+ public final fun getInstructions ()Ljava/lang/String;
+ public fun getMeta ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getResultType ()Ljava/lang/String;
+ public final fun getSupportedVersions ()Ljava/util/List;
+ public final fun getTtlMs ()J
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/DiscoverResult$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/DiscoverResult;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/DiscoverResult$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
public final class io/modelcontextprotocol/kotlin/sdk/types/DoubleSchema : io/modelcontextprotocol/kotlin/sdk/types/NumberSchemaDefinition {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/DoubleSchema$Companion;
public fun ()V
@@ -1241,7 +1579,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
@@ -1619,7 +1957,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
@@ -2064,7 +2402,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;
@@ -2454,7 +2792,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 {
@@ -2525,7 +2863,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 {
@@ -2596,7 +2934,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 {
@@ -2666,7 +3004,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 {
@@ -2800,7 +3138,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 {
@@ -2924,7 +3262,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Logging_dslKt {
public abstract interface annotation class io/modelcontextprotocol/kotlin/sdk/types/McpDsl : java/lang/annotation/Annotation {
}
-public final class io/modelcontextprotocol/kotlin/sdk/types/McpException : java/lang/Exception {
+public class io/modelcontextprotocol/kotlin/sdk/types/McpException : java/lang/Exception {
public fun (I)V
public fun (ILjava/lang/String;)V
public fun (ILjava/lang/String;Lkotlinx/serialization/json/JsonElement;)V
@@ -2934,7 +3272,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/McpException : java/
public final fun getData ()Lkotlinx/serialization/json/JsonElement;
}
-public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/MediaContent : io/modelcontextprotocol/kotlin/sdk/types/ContentBlock {
+public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/MediaContent : io/modelcontextprotocol/kotlin/sdk/types/ContentBlock, io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent$Companion;
}
@@ -3018,6 +3356,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Method$Defined : jav
public static final field ResourcesUnsubscribe Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
public static final field RootsList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
public static final field SamplingCreateMessage Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
+ public static final field ServerDiscover Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
public static final field TasksCancel Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
public static final field TasksGet Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
public static final field TasksList Lio/modelcontextprotocol/kotlin/sdk/types/Method$Defined;
@@ -3226,7 +3565,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 {
@@ -3540,6 +3879,8 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RPCError$ErrorCode {
public static final field PARSE_ERROR I
public static final field REQUEST_TIMEOUT I
public static final field RESOURCE_NOT_FOUND I
+ public static final field UNSUPPORTED_PROTOCOL_VERSION I
+ public static final field URL_ELICITATION_REQUIRED I
}
public final class io/modelcontextprotocol/kotlin/sdk/types/ReadResourceRequest : io/modelcontextprotocol/kotlin/sdk/types/ClientRequest {
@@ -3576,7 +3917,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
}
@@ -3784,8 +4125,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/RequestMeta {
public static fun equals-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/Object;)Z
public static final fun equals-impl0 (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Z
public static final fun get-impl (Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;)Lkotlinx/serialization/json/JsonElement;
+ public static final fun getClientCapabilities-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ClientCapabilities;
+ public static final fun getClientInfo-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/Implementation;
public final fun getJson ()Lkotlinx/serialization/json/JsonObject;
+ public static final fun getLogLevel-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/LoggingLevel;
public static final fun getProgressToken-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/RequestId;
+ public static final fun getProtocolVersion-impl (Lkotlinx/serialization/json/JsonObject;)Ljava/lang/String;
public static final fun getRelatedTask-impl (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/RelatedTaskMetadata;
public fun hashCode ()I
public static fun hashCode-impl (Lkotlinx/serialization/json/JsonObject;)I
@@ -4196,13 +4541,18 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Roots_dslKt {
public final class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessage {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage$Companion;
- public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;)V
+ public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent;Lkotlinx/serialization/json/JsonObject;)V
+ public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V
+ public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/Role;
- public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;
- public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage;
- public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage;Lio/modelcontextprotocol/kotlin/sdk/types/Role;Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage;
+ public final fun component2 ()Ljava/util/List;
+ public final fun component3 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage;Lio/modelcontextprotocol/kotlin/sdk/types/Role;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessage;
public fun equals (Ljava/lang/Object;)Z
- public final fun getContent ()Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;
+ public final fun getContent ()Ljava/util/List;
+ public final fun getMeta ()Lkotlinx/serialization/json/JsonObject;
public final fun getRole ()Lio/modelcontextprotocol/kotlin/sdk/types/Role;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
@@ -4230,6 +4580,15 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuild
public final fun user (Lio/modelcontextprotocol/kotlin/sdk/types/MediaContent;)V
}
+public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent : io/modelcontextprotocol/kotlin/sdk/types/WithMeta {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent$Companion;
+ public abstract fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
public final class io/modelcontextprotocol/kotlin/sdk/types/Sampling_dslKt {
public static final fun assistant (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function0;)V
public static final fun assistantAudio (Lio/modelcontextprotocol/kotlin/sdk/types/SamplingMessageBuilder;Lkotlin/jvm/functions/Function1;)V
@@ -4245,22 +4604,26 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/Sampling_dslKt {
public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Companion;
public fun ()V
- public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V
- public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)V
+ public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;
public final fun component2 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;
public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;
public final fun component4 ()Lkotlinx/serialization/json/JsonObject;
public final fun component5 ()Lkotlinx/serialization/json/JsonObject;
- public final fun component6 ()Lkotlinx/serialization/json/JsonObject;
- public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;
- public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;
+ public final fun component6 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;
+ public final fun component7 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun component8 ()Ljava/util/Map;
+ public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Ljava/util/Map;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities;
public fun equals (Ljava/lang/Object;)Z
public final fun getCompletions ()Lkotlinx/serialization/json/JsonObject;
public final fun getExperimental ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getExtensions ()Ljava/util/Map;
public final fun getLogging ()Lkotlinx/serialization/json/JsonObject;
public final fun getPrompts ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Prompts;
public final fun getResources ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Resources;
+ public final fun getTasks ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;
public final fun getTools ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
@@ -4343,6 +4706,97 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$R
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
+public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Companion;
+ public fun ()V
+ public fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;)V
+ public synthetic fun (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun component2 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun component3 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;
+ public final fun copy (Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getCancel ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getList ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getRequests ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Companion;
+ public fun ()V
+ public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;)V
+ public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;
+ public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getTools ()Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools$Companion;
+ public fun ()V
+ public fun (Lkotlinx/serialization/json/JsonObject;)V
+ public synthetic fun (Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun copy (Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getCall ()Lkotlinx/serialization/json/JsonObject;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tasks$Requests$Tools$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
public final class io/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ServerCapabilities$Tools$Companion;
public fun ()V
@@ -4430,7 +4884,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
}
@@ -4503,6 +4957,7 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/StopReason$Companion
public final fun getEndTurn--MeDoko ()Ljava/lang/String;
public final fun getMaxTokens--MeDoko ()Ljava/lang/String;
public final fun getStopSequence--MeDoko ()Ljava/lang/String;
+ public final fun getToolUse--MeDoko ()Ljava/lang/String;
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
@@ -4595,7 +5050,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
}
@@ -5072,6 +5527,49 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolAnnotations$Comp
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
+public final class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Companion;
+ public fun ()V
+ public fun (Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;)V
+ public synthetic fun (Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;
+ public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getMode ()Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode : java/lang/Enum {
+ public static final field Auto Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode$Companion;
+ public static final field None Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;
+ public static final field Required Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
+ public static fun valueOf (Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;
+ public static fun values ()[Lio/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ToolChoice$Mode$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
public final class io/modelcontextprotocol/kotlin/sdk/types/ToolExecution {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolExecution$Companion;
public fun ()V
@@ -5132,20 +5630,59 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolListChangedNotif
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
+public final class io/modelcontextprotocol/kotlin/sdk/types/ToolResultContent : io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent$Companion;
+ public fun (Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;)V
+ public synthetic fun (Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/util/List;
+ public final fun component3 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun component4 ()Ljava/lang/Boolean;
+ public final fun component5 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun copy (Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent;Ljava/lang/String;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;Ljava/lang/Boolean;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getContent ()Ljava/util/List;
+ public fun getMeta ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getStructuredContent ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getToolUseId ()Ljava/lang/String;
+ public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes;
+ public fun hashCode ()I
+ public final fun isError ()Ljava/lang/Boolean;
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolResultContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ToolResultContent;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ToolResultContent$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
public final class io/modelcontextprotocol/kotlin/sdk/types/ToolSchema {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema$Companion;
public fun ()V
- public fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V
- public synthetic fun (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public final fun component1 ()Lkotlinx/serialization/json/JsonObject;
- public final fun component2 ()Ljava/util/List;
- public final fun component3 ()Lkotlinx/serialization/json/JsonObject;
- public final fun copy (Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
- public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
+ public fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)V
+ public synthetic fun (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun component3 ()Ljava/util/List;
+ public final fun component4 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun copy (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ljava/util/List;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolSchema;
public fun equals (Ljava/lang/Object;)Z
public final fun getDefs ()Lkotlinx/serialization/json/JsonObject;
public final fun getProperties ()Lkotlinx/serialization/json/JsonObject;
public final fun getRequired ()Ljava/util/List;
+ public final fun getSchema ()Ljava/lang/String;
public final fun getType ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
@@ -5166,6 +5703,41 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/ToolSchema$Companion
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
+public final class io/modelcontextprotocol/kotlin/sdk/types/ToolUseContent : io/modelcontextprotocol/kotlin/sdk/types/SamplingMessageContent {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent$Companion;
+ public fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)V
+ public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/lang/String;
+ public final fun component3 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun component4 ()Lkotlinx/serialization/json/JsonObject;
+ public final fun copy (Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getId ()Ljava/lang/String;
+ public final fun getInput ()Lkotlinx/serialization/json/JsonObject;
+ public fun getMeta ()Lkotlinx/serialization/json/JsonObject;
+ public final fun getName ()Ljava/lang/String;
+ public fun getType ()Lio/modelcontextprotocol/kotlin/sdk/types/ContentTypes;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/ToolUseContent$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/ToolUseContent;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/ToolUseContent$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
public final class io/modelcontextprotocol/kotlin/sdk/types/ToolsKt {
public static final fun error (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult;
public static synthetic fun error$default (Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult$Companion;Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/CallToolResult;
@@ -5246,7 +5818,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
}
@@ -5281,6 +5853,35 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/UnsubscribeRequestPa
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
+public final class io/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData$Companion;
+ public fun (Ljava/util/List;Ljava/lang/String;)V
+ public final fun component1 ()Ljava/util/List;
+ public final fun component2 ()Ljava/lang/String;
+ public final fun copy (Ljava/util/List;Ljava/lang/String;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getRequested ()Ljava/lang/String;
+ public final fun getSupported ()Ljava/util/List;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/UnsupportedProtocolVersionData$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema : io/modelcontextprotocol/kotlin/sdk/types/MultiSelectEnumSchema {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Companion;
public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Lio/modelcontextprotocol/kotlin/sdk/types/UntitledMultiSelectEnumSchema$Items;Ljava/util/List;)V
@@ -5383,6 +5984,39 @@ public final class io/modelcontextprotocol/kotlin/sdk/types/UntitledSingleSelect
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
+public final class io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData {
+ public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData$Companion;
+ public fun (Ljava/util/List;)V
+ public final fun component1 ()Ljava/util/List;
+ public final fun copy (Ljava/util/List;)Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData;
+ public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData;Ljava/util/List;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getElicitations ()Ljava/util/List;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final synthetic class io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+ public static final field INSTANCE Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData$$serializer;
+ public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
+ public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredData$Companion {
+ public final fun serializer ()Lkotlinx/serialization/KSerializer;
+}
+
+public final class io/modelcontextprotocol/kotlin/sdk/types/UrlElicitationRequiredException : io/modelcontextprotocol/kotlin/sdk/types/McpException {
+ public fun (Ljava/util/List;Ljava/lang/String;)V
+ public synthetic fun (Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun getElicitations ()Ljava/util/List;
+}
+
public abstract interface class io/modelcontextprotocol/kotlin/sdk/types/WithMeta {
public static final field Companion Lio/modelcontextprotocol/kotlin/sdk/types/WithMeta$Companion;
public abstract fun getMeta ()Lkotlinx/serialization/json/JsonObject;
diff --git a/kotlin-sdk-core/build.gradle.kts b/kotlin-sdk-core/build.gradle.kts
index 0e702c4..1d19f5d 100644
--- a/kotlin-sdk-core/build.gradle.kts
+++ b/kotlin-sdk-core/build.gradle.kts
@@ -4,65 +4,10 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
id("mcp.multiplatform")
+ id("mcp.detekt")
id("mcp.publishing")
id("mcp.dokka")
alias(libs.plugins.kotlinx.binary.compatibility.validator)
- alias(libs.plugins.openapi.generator)
-}
-
-/*
-tasks.withType>().configureEach {
- dependsOn(tasks.openApiGenerate)
-}
-
-tasks.named("runKtlintCheckOverCommonMainSourceSet") {
- dependsOn(tasks.openApiGenerate)
-}
-
-// Also ensure it runs before other relevant tasks
-tasks.withType().configureEach {
- dependsOn(tasks.openApiGenerate)
-}
-
-tasks.withType().configureEach {
- if (name.lowercase().contains("sourcesjar")) {
- dependsOn(tasks.openApiGenerate)
- }
-}
- */
-
-openApiGenerate {
- val schemaVersion = "2025-03-26" // or "2025-06-18" or "draft"
- val schemaUrl =
-// "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/heads/main/schema/$schemaVersion/schema.json"
- "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/tags/$schemaVersion/schema/$schemaVersion/schema.json"
- generatorName = "kotlin"
- remoteInputSpec = schemaUrl
- outputDir = layout.buildDirectory.dir("generated-sources/openapi").get().asFile.absolutePath
- packageName = "io.modelcontextprotocol.kotlin.sdk.models"
- modelPackage = "io.modelcontextprotocol.kotlin.sdk.models"
- apiPackage = "io.modelcontextprotocol.kotlin.sdk.api"
- generateModelTests = false
- generateModelDocumentation = false
- cleanupOutput = false
- skipValidateSpec = true // do not validate spec
- library.set("multiplatform")
- ignoreFileOverride.set("${layout.projectDirectory}/.openapi-generator-ignore")
- globalProperties.set(
- mapOf(
- "supportingFiles" to "",
- "models" to "", // or generate all models
- ),
- )
- configOptions.set(
- mapOf(
- "omitGradleWrapper" to "true",
- "enumPropertyNaming" to "UPPERCASE",
- "dateLibrary" to "kotlinx-datetime",
- "explicitApi" to "true",
- "modelMutable" to "false",
- ),
- )
}
// Generation library versions
@@ -91,10 +36,8 @@ kotlin {
iosArm64()
iosX64()
iosSimulatorArm64()
- watchosX64()
watchosArm64()
watchosSimulatorArm64()
- tvosX64()
tvosArm64()
tvosSimulatorArm64()
js {
diff --git a/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml b/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml
deleted file mode 100644
index d774a5b..0000000
--- a/kotlin-sdk-core/detekt-baseline-commonMainSourceSet.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
- CyclomaticComplexMethod:jsonUtils.kt:@OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) private fun convertToJsonElement: JsonElement
- LongMethod:Protocol.kt:Protocol$public suspend fun <T : RequestResult> request: T
- MagicNumber:completion.kt:CompleteResult.Completion$100
- MatchingDeclarationName:capabilities.dsl.kt:ClientCapabilitiesBuilder
- MatchingDeclarationName:methods.kt:Method
- MaxLineLength:AbstractClientTransport.kt:AbstractClientTransport$*
- MaxLineLength:ClientTransportState.kt:ClientTransportState.Initializing$*
- MaxLineLength:Protocol.kt:Protocol$"Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}"
- MaxLineLength:Protocol.kt:Protocol$*
- MaxLineLength:Protocol.kt:RequestOptions$*
- MaxLineLength:Transport.kt:Transport$*
- MaxLineLength:TransportSendOptions.kt:TransportSendOptions$*
- ReturnCount:ReadBuffer.kt:ReadBuffer$public fun readMessage: JSONRPCMessage?
- ThrowsCount:AbstractClientTransport.kt:AbstractClientTransport$public override suspend fun send
- TooGenericExceptionCaught:AbstractClientTransport.kt:AbstractClientTransport$e: Exception
- TooGenericExceptionCaught:Protocol.kt:Protocol$cause: Throwable
- TooGenericExceptionCaught:Protocol.kt:Protocol$error: Throwable
- TooGenericExceptionCaught:Protocol.kt:Protocol$sendError: Throwable
- TooGenericExceptionCaught:ReadBuffer.kt:ReadBuffer$e: Exception
- TooGenericExceptionCaught:WebSocketMcpTransport.kt:WebSocketMcpTransport$e: Exception
- TooManyFunctions:Protocol.kt:Protocol
-
-
diff --git a/kotlin-sdk-core/detekt-baseline-main.xml b/kotlin-sdk-core/detekt-baseline-main.xml
index a22fbcd..d483d7e 100644
--- a/kotlin-sdk-core/detekt-baseline-main.xml
+++ b/kotlin-sdk-core/detekt-baseline-main.xml
@@ -2,31 +2,6 @@
- CyclomaticComplexMethod:jsonUtils.kt:@OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) private fun convertToJsonElement: JsonElement
- InjectDispatcher:utils.jvm.kt:IO
- InstanceOfCheckForException:AbstractClientTransport.kt:AbstractClientTransport$e is McpException
- LongMethod:Protocol.kt:Protocol$public suspend fun <T : RequestResult> request: T
- MagicNumber:completion.kt:CompleteResult.Completion$100
- MatchingDeclarationName:capabilities.dsl.kt:ClientCapabilitiesBuilder
- MatchingDeclarationName:methods.kt:Method
- MaxLineLength:AbstractClientTransport.kt:AbstractClientTransport$*
- MaxLineLength:ClientTransportState.kt:ClientTransportState.Initializing$*
- MaxLineLength:Protocol.kt:Protocol$"Received progress notification: token=${notification.params.progressToken}, progress=${notification.params.progress}/${notification.params.total}"
- MaxLineLength:Protocol.kt:Protocol$*
- MaxLineLength:Protocol.kt:RequestOptions$*
- MaxLineLength:Transport.kt:Transport$*
- MaxLineLength:TransportSendOptions.kt:TransportSendOptions$*
- NoNameShadowing:Protocol.kt:Protocol$error
- ReturnCount:ReadBuffer.kt:ReadBuffer$public fun readMessage: JSONRPCMessage?
- ThrowsCount:AbstractClientTransport.kt:AbstractClientTransport$public override suspend fun send
- TooGenericExceptionCaught:AbstractClientTransport.kt:AbstractClientTransport$e: Exception
- TooGenericExceptionCaught:Protocol.kt:Protocol$cause: Throwable
- TooGenericExceptionCaught:Protocol.kt:Protocol$error: Throwable
- TooGenericExceptionCaught:Protocol.kt:Protocol$sendError: Throwable
- TooGenericExceptionCaught:ReadBuffer.kt:ReadBuffer$e: Exception
- TooGenericExceptionCaught:WebSocketMcpTransport.kt:WebSocketMcpTransport$e: Exception
- TooManyFunctions:Protocol.kt:Protocol
UnsafeCallOnNullableType:Protocol.kt:Protocol$response!!
- UseCheckNotNull:Protocol.kt:Protocol$check(error != null)
diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt
index 5f67999..27013d3 100644
--- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt
+++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractClientTransport.kt
@@ -99,8 +99,10 @@ public abstract class AbstractClientTransport : AbstractTransport() {
* proceeding with further operations. It is often used to enforce state invariants within the transport's
* lifecycle mechanics, enabling robust error handling and debugging in case of unexpected state transitions.
*
- * @param expected The expected transport state. An exception will be thrown if the actual state does not match this value.
- * @throws IllegalStateException If the current state does not match the expected state.
+ * @param expected The expected transport state.
+ * An exception will be thrown if the actual state does not match this value.
+ * @param lazyMessage Function that builds the exception message from the actual state when the check fails.
+ * Defaults to a message that compares the expected state against the observed one.
*/
protected fun checkState(
expected: ClientTransportState,
diff --git a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt
index 7b88100..a738289 100644
--- a/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt
+++ b/kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt
@@ -6,11 +6,13 @@ import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
/**
- * Implements [onClose], [onError] and [onMessage] functions of [Transport] providing
- * corresponding [_onClose], [_onError] and [_onMessage] properties to use for an implementation.
+ * Base class for [Transport] implementations that handles the bookkeeping for [onClose], [onError]
+ * and [onMessage] callback registration, exposing the composed callbacks to subclasses for invocation.
+ *
+ * The callbacks are composed: each call to [onClose], [onError], or [onMessage] chains the new block
+ * after any previously registered ones.
*/
@OptIn(ExperimentalAtomicApi::class)
-@Suppress("PropertyName")
public abstract class AbstractTransport : Transport {
private val onCloseCalled = AtomicBoolean(false)
private var _onClose: (() -> Unit) = {}
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 43c0ae5..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
@@ -27,15 +27,34 @@ 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.SerializationException
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,23 +72,26 @@ public typealias ProgressCallback = (Progress) -> Unit
/**
* Additional initialization options.
*
+ * [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(
- /**
- * Whether to restrict emitted requests to only those that the remote side has indicated
- * that they can handle, through their advertised capabilities.
- *
- * Note that this DOES NOT affect checking of _local_ side capabilities, as it is
- * considered a logic error to mis-specify those.
- *
- * Currently, this defaults to false, for backwards compatibility with SDK versions
- * that did not advertise capabilities correctly.
- * In the future, this will default to true.
- */
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,
)
/**
@@ -77,21 +99,27 @@ 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.
*
- * @property relatedRequestId if present,
- * `relatedRequestId` is used to indicate to the transport which incoming request to associate this outgoing message with.
- * @property resumptionToken the resumption token used to continue long-running requests that were interrupted.
- * This allows clients to reconnect and continue from where they left off, if supported by the transport.
- * @property onResumptionToken a callback that is invoked when the resumption token changes, if supported by the transport.
- * This allows clients to persist the latest token for potential reconnection.
+ * @param relatedRequestId if present, used to indicate to the transport which incoming request to
+ * associate this outgoing message with.
+ * @param resumptionToken the resumption token used to continue long-running requests that were interrupted.
+ * Allows clients to reconnect and continue from where they left off, if supported by the transport.
+ * @param onResumptionToken callback invoked when the resumption token changes, if supported by the transport.
+ * Allows clients to persist the latest token for potential reconnection.
* @property onProgress callback for progress notifications.
- * If set, requests progress notifications from the remote end (if supported).
- * When progress notifications are received, this callback will be invoked.
+ * If set, requests progress notifications from the remote end (if supported);
+ * when progress notifications are received, this callback is invoked.
* @property timeout a timeout for this request.
- * If exceeded, a McpException with code `RequestTimeout` will be raised from request().
- * If not specified, `DEFAULT_REQUEST_TIMEOUT` will be used as the timeout.
+ * If exceeded, a [McpException] with code `RequestTimeout` is raised from [Protocol.request].
+ * If not specified, [DEFAULT_REQUEST_TIMEOUT] is used.
*/
public class RequestOptions(
relatedRequestId: RequestId? = null,
@@ -127,7 +155,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
}
@@ -138,28 +166,88 @@ 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.
*
- * @property transport the active transport, or `null` if not connected
- * @property requestHandlers registered request handlers keyed by method name
- * @property notificationHandlers registered notification handlers keyed by method name
- * @property responseHandlers pending response handlers keyed by request ID
- * @property progressHandlers registered progress callbacks keyed by progress token
+ * @property options protocol-level configuration; `null` falls back to defaults
*/
public abstract class Protocol(@PublishedApi internal val options: ProtocolOptions?) {
- public var transport: Transport? = null
- private set
+ private val connectionRef: AtomicRef = atomic(null)
+
+ /** The active transport, or `null` if not connected. */
+ public val transport: Transport?
+ get() = connectionRef.value?.transport
private val _requestHandlers:
AtomicRef RequestResult?>> =
atomic(persistentMapOf())
+
+ /** Registered request handlers keyed by method name. */
public val requestHandlers: Map<
String,
suspend (
@@ -171,20 +259,41 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
private val _notificationHandlers =
atomic(persistentMapOf Unit>())
+
+ /** Registered notification handlers keyed by method name. */
public val notificationHandlers: Map Unit>
get() = _notificationHandlers.value
private val _responseHandlers:
AtomicRef Unit>> =
atomic(persistentMapOf())
+
+ /** Pending response handlers keyed by request ID. */
public val responseHandlers: Map Unit>
get() = _responseHandlers.value
private val _progressHandlers: AtomicRef> =
atomic(persistentMapOf())
+
+ /** Registered progress callbacks keyed by progress token. */
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.
*
@@ -214,6 +323,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
@@ -224,15 +338,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 {
@@ -240,24 +403,43 @@ 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
+ recentlyCancelledRequestIds.getAndSet(persistentListOf())
+ connection.inFlightRequestJobs.getAndSet(persistentMapOf())
+ connection.handlerScope.cancel()
onClose()
val error = McpException(RPCError.ErrorCode.CONNECTION_CLOSED, "Connection closed")
@@ -266,6 +448,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}" }
@@ -277,21 +563,24 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
}
try {
handler(notification)
+ } catch (e: CancellationException) {
+ throw e
} catch (cause: Throwable) {
logger.error(cause) { "Error handling notification: ${notification.method}" }
onError(cause)
}
}
- 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(
@@ -300,6 +589,8 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
),
),
)
+ } catch (e: CancellationException) {
+ throw e
} catch (cause: Throwable) {
logger.error(cause) { "Error sending method not found response" }
onError(cause)
@@ -307,37 +598,77 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
return
}
- @Suppress("TooGenericExceptionCaught", "InstanceOfCheckForException")
- 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)
}
}
@@ -352,6 +683,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)}",
)
@@ -368,7 +703,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
}
@@ -377,8 +712,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)}",
@@ -390,8 +729,8 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
if (response != null) {
handler(response, null)
} else {
- check(error != null)
- val mcpException = McpException(
+ checkNotNull(error)
+ val mcpException = McpException.fromError(
code = error.error.code,
message = error.error.message,
data = error.error.data,
@@ -436,7 +775,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)
@@ -446,7 +786,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())
@@ -465,57 +805,99 @@ 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 {
@Suppress("UNCHECKED_CAST")
result.complete(response!!.result as T)
- } catch (error: Throwable) {
- result.completeExceptionally(error)
+ } catch (e: Throwable) {
+ result.completeExceptionally(e)
}
}
}
- 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) }
}
}
@@ -536,6 +918,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,
@@ -551,31 +938,60 @@ public abstract class Protocol(@PublishedApi internal val options: ProtocolOptio
block: suspend (T, RequestHandlerExtra) -> RequestResult?,
) {
assertRequestHandlerCapability(method)
+ val wrapped = wrapRequestHandler(method, block)
_requestHandlers.update { current ->
- current.put(method.value) { jSONRPCRequest, extraHandler ->
- val request = jSONRPCRequest.fromJSON()
- val response = block(request as T, extraHandler)
+ current.putting(method.value) { jSONRPCRequest, extraHandler ->
+ 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
}
}
}
+ /**
+ * Subclass hook to wrap an incoming-request handler before it is registered.
+ *
+ * Called once by [setRequestHandler] during registration. Subclasses may return a
+ * new function that, when invoked, performs additional checks (capability gates,
+ * schema validation, etc.) before delegating to [block]. The default implementation
+ * is the identity.
+ */
+ @Suppress("UNUSED_PARAMETER")
+ protected open fun