Compare commits
112 changed files with 675 additions and 7193 deletions
6
.github/workflows/apidocs.yaml
vendored
6
.github/workflows/apidocs.yaml
vendored
|
|
@ -24,17 +24,17 @@ jobs:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6.3.0
|
uses: gradle/actions/setup-gradle@v6
|
||||||
|
|
||||||
- name: Generate Dokka Site
|
- name: Generate Dokka Site
|
||||||
run: |-
|
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
|
name: Mark/unmark stale PRs and issues
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v11
|
- uses: actions/stale@v10
|
||||||
with:
|
with:
|
||||||
debug-only: ${{ env.DRY_RUN }}
|
debug-only: ${{ env.DRY_RUN }}
|
||||||
exempt-draft-pr: true
|
exempt-draft-pr: true
|
||||||
|
|
@ -66,7 +66,7 @@ jobs:
|
||||||
name: Close stale PRs and issues
|
name: Close stale PRs and issues
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v11
|
- uses: actions/stale@v10
|
||||||
with:
|
with:
|
||||||
debug-only: ${{ env.DRY_RUN }}
|
debug-only: ${{ env.DRY_RUN }}
|
||||||
only-labels: 'stale'
|
only-labels: 'stale'
|
||||||
|
|
|
||||||
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
|
|
@ -43,17 +43,17 @@ jobs:
|
||||||
max-workers: 2
|
max-workers: 2
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed
|
node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ jobs:
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6.3.0
|
uses: gradle/actions/setup-gradle@v6
|
||||||
with:
|
with:
|
||||||
validate-wrappers: true
|
validate-wrappers: true
|
||||||
add-job-summary: 'always'
|
add-job-summary: 'always'
|
||||||
|
|
@ -126,16 +126,16 @@ jobs:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6.3.0
|
uses: gradle/actions/setup-gradle@v6
|
||||||
with:
|
with:
|
||||||
validate-wrappers: true
|
validate-wrappers: true
|
||||||
add-job-summary: 'always'
|
add-job-summary: 'always'
|
||||||
|
|
@ -168,16 +168,16 @@ jobs:
|
||||||
name: "Build Sample: ${{ matrix.sample }}"
|
name: "Build Sample: ${{ matrix.sample }}"
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6.3.0
|
uses: gradle/actions/setup-gradle@v6
|
||||||
with:
|
with:
|
||||||
add-job-summary: 'always'
|
add-job-summary: 'always'
|
||||||
cache-read-only: true
|
cache-read-only: true
|
||||||
|
|
|
||||||
10
.github/workflows/codeql.yml
vendored
10
.github/workflows/codeql.yml
vendored
|
|
@ -25,22 +25,22 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4.38.0
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: java-kotlin
|
languages: java-kotlin
|
||||||
build-mode: manual
|
build-mode: manual
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6.3.0
|
uses: gradle/actions/setup-gradle@v6
|
||||||
with:
|
with:
|
||||||
validate-wrappers: true
|
validate-wrappers: true
|
||||||
cache-read-only: true
|
cache-read-only: true
|
||||||
|
|
@ -52,6 +52,6 @@ jobs:
|
||||||
--no-daemon --rerun-tasks --stacktrace
|
--no-daemon --rerun-tasks --stacktrace
|
||||||
|
|
||||||
- name: Analyze
|
- name: Analyze
|
||||||
uses: github/codeql-action/analyze@v4.38.0
|
uses: github/codeql-action/analyze@v4
|
||||||
with:
|
with:
|
||||||
category: '/language:java-kotlin'
|
category: '/language:java-kotlin'
|
||||||
|
|
|
||||||
24
.github/workflows/conformance.yml
vendored
24
.github/workflows/conformance.yml
vendored
|
|
@ -25,16 +25,16 @@ jobs:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6.3.0
|
uses: gradle/actions/setup-gradle@v6
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
|
|
@ -76,16 +76,16 @@ jobs:
|
||||||
- sse-retry
|
- sse-retry
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6.3.0
|
uses: gradle/actions/setup-gradle@v6
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
|
|
@ -107,16 +107,16 @@ jobs:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6.3.0
|
uses: gradle/actions/setup-gradle@v6
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
|
|
@ -146,16 +146,16 @@ jobs:
|
||||||
- auth/cross-app-access-complete-flow
|
- auth/cross-app-access-complete-flow
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6.3.0
|
uses: gradle/actions/setup-gradle@v6
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
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
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed
|
node-version: '22' # increase only after https://github.com/nodejs/node/issues/56645 will be fixed
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6.3.0
|
uses: gradle/actions/setup-gradle@v6
|
||||||
|
|
||||||
- name: Check
|
- name: Check
|
||||||
run: ./gradlew check --no-configuration-cache
|
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 }})
|
name: Build Sample (${{ matrix.sample }})
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
- name: Set up JDK ${{ env.JAVA_VERSION }}
|
||||||
uses: actions/setup-java@v6
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
distribution: ${{ env.JAVA_DISTRIBUTION }}
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v6.3.0
|
uses: gradle/actions/setup-gradle@v6
|
||||||
with:
|
with:
|
||||||
validate-wrappers: true
|
validate-wrappers: true
|
||||||
add-job-summary: 'always'
|
add-job-summary: 'always'
|
||||||
|
|
|
||||||
|
|
@ -729,7 +729,7 @@ val client = Client(
|
||||||
)
|
)
|
||||||
|
|
||||||
client.setRequestHandler<CreateMessageRequest>(Method.Defined.SamplingCreateMessage) { request, _ ->
|
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"
|
val prompt = if (content is TextContent) content.text else "your topic"
|
||||||
CreateMessageResult(
|
CreateMessageResult(
|
||||||
model = "gpt-4o-mini",
|
model = "gpt-4o-mini",
|
||||||
|
|
|
||||||
|
|
@ -416,6 +416,7 @@ potential-bugs:
|
||||||
Deprecation:
|
Deprecation:
|
||||||
active: false
|
active: false
|
||||||
aliases: ['DEPRECATION']
|
aliases: ['DEPRECATION']
|
||||||
|
excludeImportStatements: false
|
||||||
DontDowncastCollectionTypes:
|
DontDowncastCollectionTypes:
|
||||||
active: false
|
active: false
|
||||||
DoubleMutabilityForCollection:
|
DoubleMutabilityForCollection:
|
||||||
|
|
|
||||||
|
|
@ -179,13 +179,7 @@ fun Server.registerConformanceTools() {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val sampledText = result.content.joinToString("\n") { content ->
|
CallToolResult(listOf(TextContent(result.content.joinToString("\n") { it.toString() })))
|
||||||
when (content) {
|
|
||||||
is TextContent -> content.text
|
|
||||||
else -> "Non-text sampling content: ${content::class.simpleName}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CallToolResult(listOf(TextContent(sampledText)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Elicitation
|
// 9. Elicitation
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,4 @@ kotlin.daemon.jvmargs=-Xmx4G
|
||||||
kotlin.native.ignoreDisabledTargets=true
|
kotlin.native.ignoreDisabledTargets=true
|
||||||
|
|
||||||
group=io.modelcontextprotocol
|
group=io.modelcontextprotocol
|
||||||
version=0.15.0
|
version=0.13.0
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
[versions]
|
[versions]
|
||||||
# plugins version
|
# plugins version
|
||||||
atomicfu = "0.33.0"
|
atomicfu = "0.32.1"
|
||||||
binaryCompatibilityValidatorPlugin = "0.18.2"
|
binaryCompatibilityValidatorPlugin = "0.18.1"
|
||||||
detekt = "2.0.0-alpha.6"
|
detekt = "2.0.0-alpha.3"
|
||||||
dokka = "2.2.0"
|
dokka = "2.2.0"
|
||||||
kotlin = "2.4.10"
|
kotlin = "2.3.21"
|
||||||
kover = "0.9.9"
|
kover = "0.9.8"
|
||||||
ktlint = "14.2.0"
|
ktlint = "14.2.0"
|
||||||
knit = "0.5.1"
|
knit = "0.5.1"
|
||||||
mavenPublish = "0.37.0"
|
mavenPublish = "0.36.0"
|
||||||
netty = "4.2.18.Final"
|
netty = "4.2.14.Final"
|
||||||
|
|
||||||
# libraries version
|
# libraries version
|
||||||
awaitility = "4.3.0"
|
awaitility = "4.3.0"
|
||||||
collections-immutable = "0.5.2"
|
collections-immutable = "0.4.0"
|
||||||
coroutines = "1.11.0"
|
coroutines = "1.11.0"
|
||||||
kotest = "6.2.5"
|
kotest = "6.1.11"
|
||||||
kotlinx-io = "0.9.1"
|
kotlinx-io = "0.9.0"
|
||||||
ktor = "3.5.2"
|
ktor = "3.4.3"
|
||||||
logging = "8.0.4"
|
logging = "8.0.03"
|
||||||
mockk = "1.14.11"
|
mockk = "1.14.9"
|
||||||
serialization = "1.11.0"
|
serialization = "1.11.0"
|
||||||
slf4j = "2.0.19"
|
slf4j = "2.0.18"
|
||||||
logback = "1.6.3"
|
logback = "1.5.32"
|
||||||
junit = "5.14.4"
|
junit = "5.14.4"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
retries=0
|
retries=0
|
||||||
retryBackOffMs=500
|
retryBackOffMs=500
|
||||||
|
|
|
||||||
4
gradlew
vendored
4
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:
|
# Important for running:
|
||||||
#
|
#
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
# bash, then to run this script, type that shell name before the whole
|
# bash, then to run this script, type that shell name before the whole
|
||||||
# command line, like:
|
# command line, like:
|
||||||
#
|
#
|
||||||
# ksh gradlew
|
# ksh Gradle
|
||||||
#
|
#
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
# requires all of these POSIX shell features:
|
# requires all of these POSIX shell features:
|
||||||
|
|
|
||||||
4
gradlew.bat
vendored
4
gradlew.bat
vendored
|
|
@ -19,7 +19,7 @@
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem gradlew startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute gradlew
|
@rem Execute Gradle
|
||||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
@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
|
@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
|
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
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,13 @@ import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageResult
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema
|
import io.modelcontextprotocol.kotlin.sdk.types.DoubleSchema
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams
|
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestFormParams
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.ElicitRequestParams
|
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.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.EmptyJsonObject
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
|
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest
|
import io.modelcontextprotocol.kotlin.sdk.types.InitializeRequest
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult
|
import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema
|
import io.modelcontextprotocol.kotlin.sdk.types.IntegerSchema
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage
|
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.JSONRPCRequest
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse
|
import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION
|
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.ToolSchema
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema
|
import io.modelcontextprotocol.kotlin.sdk.types.UntitledMultiSelectEnumSchema
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema
|
import io.modelcontextprotocol.kotlin.sdk.types.UntitledSingleSelectEnumSchema
|
||||||
import io.modelcontextprotocol.kotlin.sdk.types.UrlElicitationRequiredException
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.awaitCancellation
|
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.joinAll
|
||||||
|
|
@ -73,7 +67,6 @@ import kotlin.test.assertIs
|
||||||
import kotlin.test.assertNull
|
import kotlin.test.assertNull
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
import kotlin.test.fail
|
import kotlin.test.fail
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
class ClientTest {
|
class ClientTest {
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -546,15 +539,6 @@ class ClientTest {
|
||||||
|
|
||||||
val serverSession = serverSessionResult.await()
|
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) { _, _ ->
|
serverSession.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { _, _ ->
|
||||||
// Simulate delay
|
// Simulate delay
|
||||||
def.complete(Unit)
|
def.complete(Unit)
|
||||||
|
|
@ -580,7 +564,6 @@ class ClientTest {
|
||||||
def.await()
|
def.await()
|
||||||
runCatching { job.cancel("Cancelled by test") }
|
runCatching { job.cancel("Cancelled by test") }
|
||||||
defCancel.await()
|
defCancel.await()
|
||||||
cancelledOnWire.await()
|
|
||||||
defTimeOut.await()
|
defTimeOut.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -619,9 +602,17 @@ class ClientTest {
|
||||||
|
|
||||||
val serverSession = serverSessionResult.await()
|
val serverSession = serverSessionResult.await()
|
||||||
serverSession.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { _, _ ->
|
serverSession.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { _, _ ->
|
||||||
// Never respond: the handler runs on a real dispatcher while the 1 ms timeout runs on the
|
// Simulate a delayed response
|
||||||
// virtual test clock, so a real-time delay could race it. Parking keeps the test deterministic.
|
// Wait ~100ms unless canceled
|
||||||
awaitCancellation()
|
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
|
// 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.
|
// Track notifications
|
||||||
val rootListChangedReceived = CompletableDeferred<Unit>()
|
var rootListChangedNotificationReceived = false
|
||||||
|
|
||||||
val serverSessionResult = CompletableDeferred<ServerSession>()
|
val serverSessionResult = CompletableDeferred<ServerSession>()
|
||||||
|
|
||||||
|
|
@ -920,13 +911,16 @@ class ClientTest {
|
||||||
serverSession.setNotificationHandler<RootsListChangedNotification>(
|
serverSession.setNotificationHandler<RootsListChangedNotification>(
|
||||||
Method.Defined.NotificationsRootsListChanged,
|
Method.Defined.NotificationsRootsListChanged,
|
||||||
) {
|
) {
|
||||||
rootListChangedReceived.complete(Unit)
|
rootListChangedNotificationReceived = true
|
||||||
CompletableDeferred(Unit)
|
CompletableDeferred(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
client.sendRootsListChanged()
|
client.sendRootsListChanged()
|
||||||
|
|
||||||
withTimeout(5.seconds) { rootListChangedReceived.await() }
|
assertTrue(
|
||||||
|
rootListChangedNotificationReceived,
|
||||||
|
"Notification should be sent when sendRootsListChanged is called",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -1124,103 +1118,6 @@ class ClientTest {
|
||||||
assertEquals(elicitationResultContent, result.content)
|
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
|
@Test
|
||||||
fun `should apply elicitation defaults for missing fields in empty content`() = runTest {
|
fun `should apply elicitation defaults for missing fields in empty content`() = runTest {
|
||||||
val schema = defaultsTestSchema()
|
val schema = defaultsTestSchema()
|
||||||
|
|
@ -1354,189 +1251,6 @@ class ClientTest {
|
||||||
client.close()
|
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(
|
private fun defaultsTestSchema(): ElicitRequestParams.RequestedSchema = ElicitRequestParams.RequestedSchema(
|
||||||
properties = mapOf(
|
properties = mapOf(
|
||||||
"name" to StringSchema(description = "User name", default = "John Doe"),
|
"name" to StringSchema(description = "User name", default = "John Doe"),
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,9 @@ import io.modelcontextprotocol.kotlin.sdk.types.McpException
|
||||||
import io.modelcontextprotocol.kotlin.test.utils.createSleepyProcessBuilder
|
import io.modelcontextprotocol.kotlin.test.utils.createSleepyProcessBuilder
|
||||||
import io.modelcontextprotocol.kotlin.test.utils.createTeeProcessBuilder
|
import io.modelcontextprotocol.kotlin.test.utils.createTeeProcessBuilder
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import kotlinx.io.Buffer
|
|
||||||
import kotlinx.io.RawSource
|
|
||||||
import kotlinx.io.asSink
|
import kotlinx.io.asSink
|
||||||
import kotlinx.io.asSource
|
import kotlinx.io.asSource
|
||||||
import kotlinx.io.buffered
|
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.assertThrows
|
||||||
import org.junit.jupiter.api.condition.DisabledOnOs
|
import org.junit.jupiter.api.condition.DisabledOnOs
|
||||||
import org.junit.jupiter.api.condition.OS
|
import org.junit.jupiter.api.condition.OS
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.concurrent.atomics.AtomicBoolean
|
import kotlin.concurrent.atomics.AtomicBoolean
|
||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertNotNull
|
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
import kotlin.test.fail
|
import kotlin.test.fail
|
||||||
import kotlin.time.Duration.Companion.seconds
|
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
|
@Test
|
||||||
fun `should read messages`() = runTest {
|
fun `should read messages`() = runTest {
|
||||||
val processBuilder = createTeeProcessBuilder()
|
val processBuilder = createTeeProcessBuilder()
|
||||||
|
|
@ -190,22 +147,4 @@ class StdioClientTransportTest : BaseTransportTest() {
|
||||||
process.waitFor()
|
process.waitFor()
|
||||||
process.destroyForcibly()
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,306 +0,0 @@
|
||||||
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<CallToolRequest>(Method.Defined.ToolsCall) { _, _ ->
|
|
||||||
val roots = serverSession.request<ListRootsResult>(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<TextContent>().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<CreateMessageRequest>(Method.Defined.SamplingCreateMessage) { _, _ ->
|
|
||||||
val tools = client.request<ListToolsResult>(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<CreateMessageResult>(
|
|
||||||
CreateMessageRequest(
|
|
||||||
CreateMessageRequestParams(
|
|
||||||
maxTokens = 100,
|
|
||||||
messages = listOf(SamplingMessage(role = Role.User, content = TextContent(text = "hi"))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val text = result.content.filterIsInstance<TextContent>().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<RequestId?>(null)
|
|
||||||
val toolHandlerCancelled = CompletableDeferred<CancellationException>()
|
|
||||||
val rootsHandlerEntered = CompletableDeferred<Unit>()
|
|
||||||
val rootsHandlerCancelled = CompletableDeferred<CancellationException>()
|
|
||||||
val parkForever = CompletableDeferred<ListRootsResult>()
|
|
||||||
val unexpectedToolCallResponse = CompletableDeferred<Unit>()
|
|
||||||
|
|
||||||
// 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<ListRootsRequest>(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<CallToolRequest>(Method.Defined.ToolsCall) { _, _ ->
|
|
||||||
val extra = currentRequestHandlerExtra()!!
|
|
||||||
capturedToolCallId.set(extra.requestId)
|
|
||||||
try {
|
|
||||||
extra.sendRequest<ListRootsResult>(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<RequestId>()
|
|
||||||
val pending = ConcurrentHashMap<RequestId, CompletableDeferred<JSONRPCMessage>>()
|
|
||||||
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<TextContent>().single().text
|
|
||||||
// The handler echoed its request id, proving currentRequestHandlerExtra() was available.
|
|
||||||
text shouldBe toolCallRequest.id.asText()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ConcurrentHashMap<RequestId, CompletableDeferred<JSONRPCMessage>>.expect(
|
|
||||||
id: RequestId,
|
|
||||||
): CompletableDeferred<JSONRPCMessage> = CompletableDeferred<JSONRPCMessage>().also { put(id, it) }
|
|
||||||
|
|
||||||
private fun RequestId.asText(): String = when (this) {
|
|
||||||
is RequestId.StringId -> value
|
|
||||||
is RequestId.NumberId -> value.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -698,8 +698,8 @@ abstract class AbstractPromptIntegrationTest : KotlinTestBase() {
|
||||||
|
|
||||||
val expectedMessage = "Prompt not found: non-existent-prompt"
|
val expectedMessage = "Prompt not found: non-existent-prompt"
|
||||||
|
|
||||||
withClue("Exception code should be INVALID_PARAMS: ${RPCError.ErrorCode.INVALID_PARAMS}") {
|
withClue("Exception code should be INTERNAL_ERROR: -32603") {
|
||||||
exception.code shouldBe RPCError.ErrorCode.INVALID_PARAMS
|
exception.code shouldBe -32603
|
||||||
}
|
}
|
||||||
withClue("Unexpected error message for non-existent prompt") {
|
withClue("Unexpected error message for non-existent prompt") {
|
||||||
exception.message shouldBe expectedMessage
|
exception.message shouldBe expectedMessage
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue