Compare commits
132 changed files with 1787 additions and 10079 deletions
6
.github/workflows/apidocs.yaml
vendored
6
.github/workflows/apidocs.yaml
vendored
|
|
@ -24,17 +24,17 @@ jobs:
|
|||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
||||
uses: actions/setup-java@v6
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||
cache: gradle
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6.3.0
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Generate Dokka Site
|
||||
run: |-
|
||||
|
|
|
|||
4
.github/workflows/automations.yml
vendored
4
.github/workflows/automations.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
name: Mark/unmark stale PRs and issues
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v11
|
||||
- uses: actions/stale@v10
|
||||
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@v11
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
debug-only: ${{ env.DRY_RUN }}
|
||||
only-labels: 'stale'
|
||||
|
|
|
|||
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
|
|
@ -43,17 +43,17 @@ jobs:
|
|||
max-workers: 2
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
||||
uses: actions/setup-java@v6
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||
|
||||
- name: Set up Node.js
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@v6
|
||||
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.3.0
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
validate-wrappers: true
|
||||
add-job-summary: 'always'
|
||||
|
|
@ -126,16 +126,16 @@ jobs:
|
|||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
||||
uses: actions/setup-java@v6
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6.3.0
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
validate-wrappers: true
|
||||
add-job-summary: 'always'
|
||||
|
|
@ -168,16 +168,16 @@ jobs:
|
|||
name: "Build Sample: ${{ matrix.sample }}"
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
||||
uses: actions/setup-java@v6
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6.3.0
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
add-job-summary: 'always'
|
||||
cache-read-only: true
|
||||
|
|
|
|||
10
.github/workflows/codeql.yml
vendored
10
.github/workflows/codeql.yml
vendored
|
|
@ -25,22 +25,22 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
||||
uses: actions/setup-java@v6
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4.38.0
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: java-kotlin
|
||||
build-mode: manual
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6.3.0
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
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.38.0
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: '/language:java-kotlin'
|
||||
|
|
|
|||
24
.github/workflows/conformance.yml
vendored
24
.github/workflows/conformance.yml
vendored
|
|
@ -25,16 +25,16 @@ jobs:
|
|||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v6
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6.3.0
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
|
|
@ -76,16 +76,16 @@ jobs:
|
|||
- sse-retry
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v6
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6.3.0
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
|
|
@ -107,16 +107,16 @@ jobs:
|
|||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v6
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6.3.0
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
|
|
@ -146,16 +146,16 @@ jobs:
|
|||
- auth/cross-app-access-complete-flow
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v6
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6.3.0
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
|
|
|
|||
8
.github/workflows/gradle-publish.yml
vendored
8
.github/workflows/gradle-publish.yml
vendored
|
|
@ -17,20 +17,20 @@ jobs:
|
|||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v6
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6.3.0
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Check
|
||||
run: ./gradlew check --no-configuration-cache
|
||||
|
|
|
|||
6
.github/workflows/samples.yml
vendored
6
.github/workflows/samples.yml
vendored
|
|
@ -36,16 +36,16 @@ jobs:
|
|||
name: Build Sample (${{ matrix.sample }})
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
||||
uses: actions/setup-java@v6
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6.3.0
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
validate-wrappers: true
|
||||
add-job-summary: 'always'
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -56,7 +56,7 @@ node_modules
|
|||
dist
|
||||
|
||||
### SWE agents ###
|
||||
.claude/
|
||||
.claude/settings.local.json
|
||||
.junie/
|
||||
|
||||
### Conformance test results ###
|
||||
|
|
|
|||
|
|
@ -729,7 +729,7 @@ val client = Client(
|
|||
)
|
||||
|
||||
client.setRequestHandler<CreateMessageRequest>(Method.Defined.SamplingCreateMessage) { request, _ ->
|
||||
val content = request.messages.lastOrNull()?.content?.firstOrNull()
|
||||
val content = request.messages.lastOrNull()?.content
|
||||
val prompt = if (content is TextContent) content.text else "your topic"
|
||||
CreateMessageResult(
|
||||
model = "gpt-4o-mini",
|
||||
|
|
|
|||
|
|
@ -416,6 +416,7 @@ potential-bugs:
|
|||
Deprecation:
|
||||
active: false
|
||||
aliases: ['DEPRECATION']
|
||||
excludeImportStatements: false
|
||||
DontDowncastCollectionTypes:
|
||||
active: false
|
||||
DoubleMutabilityForCollection:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ 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()
|
||||
|
|
|
|||
|
|
@ -179,13 +179,7 @@ fun Server.registerConformanceTools() {
|
|||
),
|
||||
),
|
||||
)
|
||||
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)))
|
||||
CallToolResult(listOf(TextContent(result.content.joinToString("\n") { it.toString() })))
|
||||
}
|
||||
|
||||
// 9. Elicitation
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@ kotlin.daemon.jvmargs=-Xmx4G
|
|||
kotlin.native.ignoreDisabledTargets=true
|
||||
|
||||
group=io.modelcontextprotocol
|
||||
version=0.15.0
|
||||
version=0.12.0
|
||||
|
|
|
|||
|
|
@ -1,29 +1,30 @@
|
|||
[versions]
|
||||
# plugins version
|
||||
atomicfu = "0.33.0"
|
||||
binaryCompatibilityValidatorPlugin = "0.18.2"
|
||||
detekt = "2.0.0-alpha.6"
|
||||
atomicfu = "0.32.1"
|
||||
binaryCompatibilityValidatorPlugin = "0.18.1"
|
||||
detekt = "2.0.0-alpha.3"
|
||||
dokka = "2.2.0"
|
||||
kotlin = "2.4.10"
|
||||
kover = "0.9.9"
|
||||
kotlin = "2.3.21"
|
||||
kover = "0.9.8"
|
||||
ktlint = "14.2.0"
|
||||
knit = "0.5.1"
|
||||
mavenPublish = "0.37.0"
|
||||
netty = "4.2.18.Final"
|
||||
mavenPublish = "0.36.0"
|
||||
netty = "4.2.12.Final"
|
||||
|
||||
# libraries version
|
||||
awaitility = "4.3.0"
|
||||
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"
|
||||
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.11.0"
|
||||
slf4j = "2.0.19"
|
||||
logback = "1.6.3"
|
||||
junit = "5.14.4"
|
||||
slf4j = "2.0.17"
|
||||
logback = "1.5.32"
|
||||
junit = "5.14.3"
|
||||
|
||||
[libraries]
|
||||
# Plugins
|
||||
|
|
@ -64,6 +65,7 @@ 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" }
|
||||
|
|
|
|||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,9 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
6
gradlew
vendored
6
gradlew
vendored
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
##############################################################################
|
||||
#
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
# Gradle 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 gradlew
|
||||
# ksh Gradle
|
||||
#
|
||||
# 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/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/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/.
|
||||
|
|
|
|||
175
gradlew.bat
vendored
175
gradlew.bat
vendored
|
|
@ -1,82 +1,93 @@
|
|||
@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%
|
||||
@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
|
||||
|
|
|
|||
|
|
@ -14,17 +14,13 @@ 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
|
||||
|
|
@ -50,10 +46,8 @@ 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
|
||||
|
|
@ -73,7 +67,6 @@ 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
|
||||
|
|
@ -546,15 +539,6 @@ 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<Unit>()
|
||||
serverTransport.onMessage { message ->
|
||||
if (message is JSONRPCNotification && message.method == Method.Defined.NotificationsCancelled.value) {
|
||||
cancelledOnWire.complete(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
serverSession.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { _, _ ->
|
||||
// Simulate delay
|
||||
def.complete(Unit)
|
||||
|
|
@ -580,7 +564,6 @@ class ClientTest {
|
|||
def.await()
|
||||
runCatching { job.cancel("Cancelled by test") }
|
||||
defCancel.await()
|
||||
cancelledOnWire.await()
|
||||
defTimeOut.await()
|
||||
}
|
||||
|
||||
|
|
@ -619,9 +602,17 @@ class ClientTest {
|
|||
|
||||
val serverSession = serverSessionResult.await()
|
||||
serverSession.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { _, _ ->
|
||||
// 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()
|
||||
// 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())
|
||||
}
|
||||
|
||||
// Request with 1 msec timeout should fail immediately
|
||||
|
|
@ -900,8 +891,8 @@ class ClientTest {
|
|||
),
|
||||
)
|
||||
|
||||
// The handler runs on Dispatchers.Default after concurrent dispatch is enabled; await the deferred.
|
||||
val rootListChangedReceived = CompletableDeferred<Unit>()
|
||||
// Track notifications
|
||||
var rootListChangedNotificationReceived = false
|
||||
|
||||
val serverSessionResult = CompletableDeferred<ServerSession>()
|
||||
|
||||
|
|
@ -920,13 +911,16 @@ class ClientTest {
|
|||
serverSession.setNotificationHandler<RootsListChangedNotification>(
|
||||
Method.Defined.NotificationsRootsListChanged,
|
||||
) {
|
||||
rootListChangedReceived.complete(Unit)
|
||||
rootListChangedNotificationReceived = true
|
||||
CompletableDeferred(Unit)
|
||||
}
|
||||
|
||||
client.sendRootsListChanged()
|
||||
|
||||
withTimeout(5.seconds) { rootListChangedReceived.await() }
|
||||
assertTrue(
|
||||
rootListChangedNotificationReceived,
|
||||
"Notification should be sent when sendRootsListChanged is called",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -1064,7 +1058,7 @@ class ClientTest {
|
|||
Implementation(name = "test client", version = "1.0"),
|
||||
ClientOptions(
|
||||
capabilities = ClientCapabilities(
|
||||
elicitation = ClientCapabilities.Elicitation(),
|
||||
elicitation = EmptyJsonObject,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -1124,103 +1118,6 @@ 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<McpException> {
|
||||
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<McpException> {
|
||||
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()
|
||||
|
|
@ -1354,189 +1251,6 @@ 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<ElicitRequestURLParams>(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<ServerSession>()
|
||||
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<IllegalArgumentException> {
|
||||
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<ElicitationCompleteNotification>()
|
||||
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<ServerSession>()
|
||||
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<IllegalArgumentException> {
|
||||
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<IllegalStateException> {
|
||||
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<ServerSession>()
|
||||
listOf(
|
||||
launch { client.connect(clientTransport) },
|
||||
launch { serverSessionResult.complete(server.createSession(serverTransport)) },
|
||||
).joinAll()
|
||||
serverSessionResult.await()
|
||||
|
||||
val exception = assertFailsWith<UrlElicitationRequiredException> {
|
||||
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"),
|
||||
|
|
@ -1556,7 +1270,7 @@ class ClientTest {
|
|||
): Pair<Client, ServerSession> = kotlinx.coroutines.coroutineScope {
|
||||
val client = Client(
|
||||
Implementation(name = "test client", version = "1.0"),
|
||||
ClientOptions(capabilities = ClientCapabilities(elicitation = ClientCapabilities.Elicitation())),
|
||||
ClientOptions(capabilities = ClientCapabilities(elicitation = EmptyJsonObject)),
|
||||
)
|
||||
client.setElicitationHandler(handler)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,9 @@ 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
|
||||
|
|
@ -22,12 +18,10 @@ 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
|
||||
|
|
@ -115,43 +109,6 @@ 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()
|
||||
|
|
@ -190,22 +147,4 @@ 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue