Compare commits

..
132 changed files with 1787 additions and 10079 deletions

View file

@ -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: |-

View file

@ -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'

View file

@ -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

View file

@ -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'

View file

@ -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' }}

View file

@ -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

View file

@ -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'

2
.gitignore vendored
View file

@ -56,7 +56,7 @@ node_modules
dist dist
### SWE agents ### ### SWE agents ###
.claude/ .claude/settings.local.json
.junie/ .junie/
### Conformance test results ### ### Conformance test results ###

View file

@ -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",

View file

@ -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:

View file

@ -12,6 +12,8 @@ fun main() {
val port = System.getenv("MCP_PORT")?.toIntOrNull() ?: 3001 val port = System.getenv("MCP_PORT")?.toIntOrNull() ?: 3001
embeddedServer(CIO, port = port) { embeddedServer(CIO, port = port) {
mcpStreamableHttp( mcpStreamableHttp(
enableDnsRebindingProtection = true,
allowedHosts = listOf("localhost", "127.0.0.1", "localhost:$port", "127.0.0.1:$port"),
eventStore = InMemoryEventStore(), eventStore = InMemoryEventStore(),
) { ) {
createConformanceServer() createConformanceServer()

View file

@ -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

View file

@ -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.12.0

View file

@ -1,29 +1,30 @@
[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.12.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.10.2"
kotest = "6.2.5" kotest = "6.1.11"
kotlinx-io = "0.9.1" kotlinx-io = "0.9.0"
ktor = "3.5.2" ktor = "3.3.3"
logging = "8.0.4" logging = "8.0.01"
mockk = "1.14.11" mockk = "1.14.9"
mokksy = "0.8.1"
serialization = "1.11.0" serialization = "1.11.0"
slf4j = "2.0.19" slf4j = "2.0.17"
logback = "1.6.3" logback = "1.5.32"
junit = "5.14.4" junit = "5.14.3"
[libraries] [libraries]
# Plugins # 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-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" } ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" } 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" } netty-bom = { group = "io.netty", name = "netty-bom", version.ref = "netty" }
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }

Binary file not shown.

View file

@ -1,9 +1,7 @@
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.4.1-bin.zip
networkTimeout=10000 networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

6
gradlew vendored
View file

@ -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:
@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (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. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.

35
gradlew.bat vendored
View file

@ -19,12 +19,12 @@
@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 ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled @rem Set local scope for the variables with windows NT shell
setlocal EnableExtensions if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 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 echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1 goto fail
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@ -65,18 +65,29 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 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 echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1 goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
@rem Execute gradlew @rem Execute Gradle
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
@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 :end
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts @rem End local scope for the variables with windows NT shell
"%COMSPEC%" /c exit %ERRORLEVEL% 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

View file

@ -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
@ -1064,7 +1058,7 @@ class ClientTest {
Implementation(name = "test client", version = "1.0"), Implementation(name = "test client", version = "1.0"),
ClientOptions( ClientOptions(
capabilities = ClientCapabilities( capabilities = ClientCapabilities(
elicitation = ClientCapabilities.Elicitation(), elicitation = EmptyJsonObject,
), ),
), ),
) )
@ -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"),
@ -1556,7 +1270,7 @@ class ClientTest {
): Pair<Client, ServerSession> = kotlinx.coroutines.coroutineScope { ): Pair<Client, ServerSession> = kotlinx.coroutines.coroutineScope {
val client = Client( val client = Client(
Implementation(name = "test client", version = "1.0"), Implementation(name = "test client", version = "1.0"),
ClientOptions(capabilities = ClientCapabilities(elicitation = ClientCapabilities.Elicitation())), ClientOptions(capabilities = ClientCapabilities(elicitation = EmptyJsonObject)),
) )
client.setElicitationHandler(handler) client.setElicitationHandler(handler)

View file

@ -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()
}
}
} }

Some files were not shown because too many files have changed in this diff Show more