From 288d46cdde0ef643d99ea804b0ae20f4361603de Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:48:31 +0200 Subject: [PATCH] fix: Update/Fix build workflow (#474) # Update/Fix build workflow - `macos-latest-xlarge` is no longer available. Switch to cheaper runner: `macos-latest-xlarge` -> `macos-latest` - Refactor build to run matrix job under Linux and macOS - Tune caching GH caching - Add junit and logger properties - Downgrade Junit to 5.14.2 - Extract `ProcessBuilder` creation logic into helper functions - Disable `StdioClientTransportTest` on windows temporary ## How Has This Been Tested? CI ## Breaking Changes Functionally - no. Outdated/broken TypeScript tests were disabled ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [x] CI Update ## Checklist - [ ] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [ ] My code follows the repository's style guidelines - [ ] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context GitHub actions workflows were blocked because the selected "macos-latest-xlarge" runner, which is not available for the free open-source plan. With "macos-latest" workflow works. --- .github/workflows/apidocs.yaml | 2 +- .github/workflows/build.yml | 162 +++++-- .github/workflows/codeql.yml | 28 +- .github/workflows/conformance.yml | 2 +- .github/workflows/gradle-publish.yml | 5 +- .github/workflows/samples.yml | 20 +- .../test/resources/simplelogger.properties | 8 + gradle/libs.versions.toml | 2 +- .../resources/junit-platform.properties | 6 + .../jvmTest/resources/simplelogger.properties | 8 + .../jvmTest/resources/simplelogger.properties | 8 + kotlin-sdk-test/build.gradle.kts | 1 + .../kotlin/sdk/shared/BaseTransportTest.kt | 2 +- .../sdk/client/StdioClientTransportTest.kt | 17 +- .../kotlin/sdk/client/processBuilders.kt | 15 + .../AbstractKotlinClientTsServerTest.kt | 2 + .../AbstractTsClientKotlinServerTest.kt | 2 + ...dSchemaAbstractKotlinClientTsServerTest.kt | 2 +- .../typescript/OldSchemaTsTestBase.kt | 455 ------------------ .../KotlinClientTsServerEdgeCasesTestSse.kt | 2 + .../sse/KotlinClientTsServerTestSse.kt | 2 + ...emaKotlinClientTsServerEdgeCasesTestSse.kt | 10 +- .../OldSchemaKotlinClientTsServerTestSse.kt | 6 +- .../sse/TsClientKotlinServerTestSse.kt | 2 + .../typescript/sse/TsEdgeCasesTestSse.kt | 2 + .../KotlinClientTsServerEdgeCasesTestStdio.kt | 2 + .../stdio/KotlinClientTsServerTestStdio.kt | 2 + ...aKotlinClientTsServerEdgeCasesTestStdio.kt | 10 +- .../OldSchemaKotlinClientTsServerTestStdio.kt | 6 +- .../stdio/TsClientKotlinServerTestStdio.kt | 2 + .../typescript/stdio/TsEdgeCasesTestStdio.kt | 2 + .../sdk/integration/utils/DisabledOnCI.kt | 6 + .../jvmTest/resources/simplelogger.properties | 8 + 33 files changed, 268 insertions(+), 541 deletions(-) create mode 100644 conformance-test/src/test/resources/simplelogger.properties create mode 100644 kotlin-sdk-core/src/jvmTest/resources/junit-platform.properties create mode 100644 kotlin-sdk-core/src/jvmTest/resources/simplelogger.properties create mode 100644 kotlin-sdk-server/src/jvmTest/resources/simplelogger.properties create mode 100644 kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/processBuilders.kt delete mode 100644 kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/OldSchemaTsTestBase.kt create mode 100644 kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/DisabledOnCI.kt create mode 100644 kotlin-sdk-test/src/jvmTest/resources/simplelogger.properties diff --git a/.github/workflows/apidocs.yaml b/.github/workflows/apidocs.yaml index a22acb2..603ab3a 100644 --- a/.github/workflows/apidocs.yaml +++ b/.github/workflows/apidocs.yaml @@ -1,4 +1,4 @@ -name: Build and Publish API Docs to GitHub Pages +name: Publish API Docs on: release: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99ac3b8..fa07e46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,69 +10,88 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} # Cancel only when the run is NOT on `main` branch - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: checks: write pull-requests: write # only required if `comment: true` was enabled +env: + JAVA_VERSION: 21 + JAVA_DISTRIBUTION: temurin + JAVA_OPTS: "-Dfile.encoding=UTF-8 -Djava.awt.headless=true" + GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy=in-process" + jobs: build: - runs-on: macos-latest-xlarge - name: Build - timeout-minutes: 20 - env: - JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" + runs-on: ${{ matrix.os }} + name: Build (${{ matrix.job-name }}) + timeout-minutes: 30 + permissions: + contents: read + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + job-name: "Linux Tests" + gradle-tasks: "build ktlintCheck koverLog koverHtmlReport publishToMavenLocal -Pversion=1-SNAPSHOT" + max-workers: 3 + + - os: windows-latest + job-name: "Windows Tests" + gradle-tasks: "jvmTest" + max-workers: 3 + + - os: macos-latest + job-name: "iOS Simulator tests" + gradle-tasks: "linkIosSimulatorArm64 iosSimulatorArm64Test" + max-workers: 2 + steps: - uses: actions/checkout@v6 - - name: Set up JDK 21 + - name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-java@v5 with: - java-version: '21' - distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Set up Node.js + if: matrix.os == 'ubuntu-latest' + uses: actions/setup-node@v6 + with: + node-version: 'lts/*' - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 with: + validate-wrappers: true add-job-summary: 'always' - cache-read-only: true + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + gradle-home-cache-includes: | + caches + notifications + sdks + .konan/** - - name: Build with Gradle + - name: ${{ matrix.job-name }} run: |- - ./gradlew --no-daemon \ - --rerun-tasks \ - clean \ - ktlintCheck \ - build \ - -x :conformance-test:test \ - koverLog koverHtmlReport \ - publishToMavenLocal \ - -Pversion=0.0.1-SNAPSHOT - - - name: Build Kotlin-MCP-Client Sample - working-directory: ./samples/kotlin-mcp-client - run: ./gradlew --no-daemon clean build -Pmcp.kotlin.overrideVersion=0.0.1-SNAPSHOT - - - name: Build Kotlin-MCP-Server Sample - working-directory: ./samples/kotlin-mcp-server - run: ./gradlew --no-daemon clean build -Pmcp.kotlin.overrideVersion=0.0.1-SNAPSHOT - - - name: Build Weather-Stdio-Server Sample - working-directory: ./samples/weather-stdio-server - run: ./gradlew --no-daemon clean build -Pmcp.kotlin.overrideVersion=0.0.1-SNAPSHOT + ./gradlew ${{ matrix.gradle-tasks }} -x :conformance-test:test --max-workers ${{ matrix.max-workers }} --continue --configure-on-demand --parallel - name: Upload Reports - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v5 + if: always() + uses: actions/upload-artifact@v6 with: - name: reports + name: reports-build-${{ matrix.os }} path: | **/build/reports/ - name: Publish Test Report uses: mikepenz/action-junit-report@v6 - if: ${{ !cancelled() }} # always run even if the previous step fails + if: always() with: annotate_only: true detailed_summary: true @@ -83,9 +102,68 @@ jobs: report_paths: '**/test-results/**/TEST-*.xml' truncate_stack_traces: false - - name: Disable Auto-Merge on Fail - if: failure() && github.event_name == 'pull_request' - run: gh pr merge --disable-auto "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Archive Maven Local Repository + if: ${{ success() && matrix.os == 'ubuntu-latest' }} + run: | + cd ~/.m2/repository && zip -r $GITHUB_WORKSPACE/local-maven-repository.zip . + + - name: Upload Maven Local Repository + if: ${{ success() && matrix.os == 'ubuntu-latest' }} + uses: actions/upload-artifact@v4 + with: + name: local-maven-repository + path: local-maven-repository.zip + retention-days: 1 + + samples: + needs: [ build ] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sample: + - kotlin-mcp-client + - kotlin-mcp-server + - weather-stdio-server + + name: "Build Sample: ${{ matrix.sample }}" + timeout-minutes: 10 + env: + JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" + steps: + - uses: actions/checkout@v6 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v5 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + add-job-summary: 'always' + cache-read-only: true + + - name: Download Maven Local Repository + uses: actions/download-artifact@v7 + with: + name: local-maven-repository + path: . + + - name: Extract Maven Local Repository + run: | + mkdir -p ~/.m2/repository + unzip -o local-maven-repository.zip -d ~/.m2/repository + + - name: "Build Sample: ${{ matrix.sample }}" + working-directory: ./samples/${{ matrix.sample }} + run: ./gradlew build -Pmcp.kotlin.overrideVersion=1-SNAPSHOT + + - name: Upload Reports + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v6 + with: + name: reports-samples-snapshot-${{ matrix.sample }} + path: | + **/build/reports/ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1a7789d..0daaf5f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,10 +8,19 @@ on: schedule: - cron: '0 4 * * 0' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +env: + JAVA_VERSION: 21 + JAVA_DISTRIBUTION: temurin + jobs: analyze: name: Analyze (${{ matrix.language }}) runs-on: ubuntu-latest + timeout-minutes: 30 permissions: # required for all workflows security-events: write @@ -24,6 +33,7 @@ jobs: contents: read strategy: + fail-fast: false matrix: language: [ java-kotlin ] @@ -33,10 +43,11 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-java@v5 + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v5 with: - distribution: temurin - java-version: '21' + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.JAVA_VERSION }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL @@ -45,16 +56,15 @@ jobs: languages: ${{ matrix.language }} build-mode: manual - - uses: actions/cache@v4 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + validate-wrappers: true + cache-read-only: true - name: Build Kotlin sources run: | - ./gradlew \ + ./gradlew \ :kotlin-sdk-core:compileKotlinJvm \ :kotlin-sdk-client:compileKotlinJvm \ :kotlin-sdk-server:compileKotlinJvm \ diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index a6a8405..5aee68b 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -14,7 +14,7 @@ concurrency: jobs: run-conformance: - runs-on: macos-latest-xlarge + runs-on: macos-latest name: Run Conformance Tests timeout-minutes: 20 env: diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index bb81dbe..43f4766 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -13,10 +13,11 @@ on: jobs: build: - runs-on: macos-latest-xlarge + runs-on: macos-latest environment: release env: - JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" + JAVA_OPTS: "-Xmx4g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=768m -Djava.awt.headless=true" + GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy=in-process" permissions: contents: write diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 472110d..199a0a0 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -16,43 +16,49 @@ concurrency: # Cancel only when the run is NOT on `main` branch cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +env: + JAVA_VERSION: 21 + JAVA_DISTRIBUTION: temurin + jobs: build: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: sample: - kotlin-mcp-client - kotlin-mcp-server - weather-stdio-server - name: Build Sample + name: Build Sample (${{ matrix.sample }}) timeout-minutes: 10 env: JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" steps: - uses: actions/checkout@v6 - - name: Set up JDK 21 + - name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-java@v5 with: - java-version: 21 - distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 with: + validate-wrappers: true add-job-summary: 'always' - cache-read-only: true + cache-read-only: ${{ github.ref != 'refs/heads/main' }} - name: "Build Sample: ${{ matrix.sample }}" working-directory: ./samples/${{ matrix.sample }} - run: ./gradlew --no-daemon clean build + run: ./gradlew --no-daemon build - name: Upload Reports if: ${{ !cancelled() }} uses: actions/upload-artifact@v5 with: - name: reports-${{ matrix.sample }} + name: reports-samples-${{ matrix.sample }} path: | **/build/reports/ diff --git a/conformance-test/src/test/resources/simplelogger.properties b/conformance-test/src/test/resources/simplelogger.properties new file mode 100644 index 0000000..95fb646 --- /dev/null +++ b/conformance-test/src/test/resources/simplelogger.properties @@ -0,0 +1,8 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.ktor.server=DEBUG +org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d5a19ee..30f3356 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ mockk = "1.14.7" mokksy = "0.6.2" serialization = "1.9.0" slf4j = "2.0.17" -junit="6.0.1" +junit="5.14.2" [libraries] # Plugins diff --git a/kotlin-sdk-core/src/jvmTest/resources/junit-platform.properties b/kotlin-sdk-core/src/jvmTest/resources/junit-platform.properties new file mode 100644 index 0000000..9af7987 --- /dev/null +++ b/kotlin-sdk-core/src/jvmTest/resources/junit-platform.properties @@ -0,0 +1,6 @@ +## https://docs.junit.org/5.3.0-M1/user-guide/index.html#writing-tests-parallel-execution +junit.jupiter.execution.parallel.enabled=true +junit.jupiter.execution.parallel.config.strategy=dynamic +junit.jupiter.execution.parallel.mode.default=concurrent +junit.jupiter.execution.parallel.mode.classes.default=concurrent +junit.jupiter.execution.timeout.default=2m diff --git a/kotlin-sdk-core/src/jvmTest/resources/simplelogger.properties b/kotlin-sdk-core/src/jvmTest/resources/simplelogger.properties new file mode 100644 index 0000000..95fb646 --- /dev/null +++ b/kotlin-sdk-core/src/jvmTest/resources/simplelogger.properties @@ -0,0 +1,8 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.ktor.server=DEBUG +org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG diff --git a/kotlin-sdk-server/src/jvmTest/resources/simplelogger.properties b/kotlin-sdk-server/src/jvmTest/resources/simplelogger.properties new file mode 100644 index 0000000..95fb646 --- /dev/null +++ b/kotlin-sdk-server/src/jvmTest/resources/simplelogger.properties @@ -0,0 +1,8 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.ktor.server=DEBUG +org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG diff --git a/kotlin-sdk-test/build.gradle.kts b/kotlin-sdk-test/build.gradle.kts index 252d0e2..fce90be 100644 --- a/kotlin-sdk-test/build.gradle.kts +++ b/kotlin-sdk-test/build.gradle.kts @@ -26,6 +26,7 @@ kotlin { jvmTest { dependencies { implementation(libs.awaitility) + implementation(libs.junit.jupiter.params) runtimeOnly(libs.slf4j.simple) } } diff --git a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt index 5de5fd2..d5849eb 100644 --- a/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt +++ b/kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt @@ -37,7 +37,7 @@ abstract class BaseTransportTest { fail("Unexpected error: $error") } - val messages = listOf( + val messages = listOf( PingRequest().toJSON(), InitializedNotification().toJSON(), ) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt index fa2f032..ecd21f3 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StdioClientTransportTest.kt @@ -12,6 +12,8 @@ import kotlinx.io.buffered import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.condition.DisabledOnOs +import org.junit.jupiter.api.condition.OS import java.util.concurrent.TimeUnit import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi @@ -22,15 +24,12 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @Timeout(30, unit = TimeUnit.SECONDS) +@DisabledOnOs(OS.WINDOWS) // TODO: fix running on windows class StdioClientTransportTest : BaseTransportTest() { @Test fun `handle stdio error`(): Unit = runBlocking { - val processBuilder = if (System.getProperty("os.name").lowercase().contains("win")) { - ProcessBuilder("cmd", "/c", "pause 1 && echo simulated error 1>&2 && exit 1") - } else { - ProcessBuilder("sh", "-c", "sleep 1 && echo 'simulated error' >&2 && exit 1") - } + val processBuilder = createSleepyProcessBuilder() val process = processBuilder.start() @@ -65,8 +64,8 @@ class StdioClientTransportTest : BaseTransportTest() { @OptIn(ExperimentalAtomicApi::class) @Test fun `should start then close cleanly`() = runTest { - // Run process "/usr/bin/tee" - val processBuilder = ProcessBuilder("/usr/bin/tee") + // Run loopback process + val processBuilder = createTeeProcessBuilder() val process = processBuilder.start() val input = process.inputStream.asSource().buffered() @@ -102,7 +101,7 @@ class StdioClientTransportTest : BaseTransportTest() { @Test fun `should read messages`() = runTest { - val processBuilder = ProcessBuilder("/usr/bin/tee") + val processBuilder = createTeeProcessBuilder() val process = processBuilder.start() val input = process.inputStream.asSource().buffered() @@ -121,7 +120,7 @@ class StdioClientTransportTest : BaseTransportTest() { @Test fun `should ignore first output messages`() = runTest { - val processBuilder = ProcessBuilder("/usr/bin/tee") + val processBuilder = createTeeProcessBuilder() val process = processBuilder.start() process.outputStream.write("Stdio server started".toByteArray()) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/processBuilders.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/processBuilders.kt new file mode 100644 index 0000000..405f2b3 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/processBuilders.kt @@ -0,0 +1,15 @@ +package io.modelcontextprotocol.kotlin.sdk.client + +private val isWindows = System.getProperty("os.name").lowercase().contains("win") + +fun createTeeProcessBuilder(): ProcessBuilder = if (isWindows) { + ProcessBuilder("cmd", "/c", "findstr", "^") +} else { + ProcessBuilder("tee") +} + +fun createSleepyProcessBuilder(): ProcessBuilder = if (isWindows) { + ProcessBuilder("cmd", "/c", "ping -n 2 127.0.0.1 > nul && echo simulated error 1>&2 && exit 1") +} else { + ProcessBuilder("sh", "-c", "sleep 1 && echo 'simulated error' >&2 && exit 1") +} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerTest.kt index 5d04512..02a4cda 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -11,6 +12,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue +@DisabledOnCI abstract class AbstractKotlinClientTsServerTest : TsTestBase() { protected abstract suspend fun useClient(block: suspend (Client) -> T): T diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractTsClientKotlinServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractTsClientKotlinServerTest.kt index e7950e7..2b77ed5 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractTsClientKotlinServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractTsClientKotlinServerTest.kt @@ -1,11 +1,13 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout import java.util.concurrent.TimeUnit import kotlin.test.assertTrue +@DisabledOnCI abstract class AbstractTsClientKotlinServerTest : TsTestBase() { protected open fun beforeServer() {} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/OldSchemaAbstractKotlinClientTsServerTest.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/OldSchemaAbstractKotlinClientTsServerTest.kt index 64e114f..5d0406c 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/OldSchemaAbstractKotlinClientTsServerTest.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/OldSchemaAbstractKotlinClientTsServerTest.kt @@ -11,7 +11,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue -abstract class OldSchemaAbstractKotlinClientTsServerTest : OldSchemaTsTestBase() { +abstract class OldSchemaAbstractKotlinClientTsServerTest : TsTestBase() { protected abstract suspend fun useClient(block: suspend (Client) -> T): T @Test diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/OldSchemaTsTestBase.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/OldSchemaTsTestBase.kt deleted file mode 100644 index 98d83d3..0000000 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/OldSchemaTsTestBase.kt +++ /dev/null @@ -1,455 +0,0 @@ -package io.modelcontextprotocol.kotlin.sdk.integration.typescript - -import io.ktor.client.HttpClient -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.sse.SSE -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport -import io.modelcontextprotocol.kotlin.sdk.client.mcpStreamableHttp -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse.KotlinServerForTsClient -import io.modelcontextprotocol.kotlin.sdk.integration.utils.Retry -import io.modelcontextprotocol.kotlin.sdk.server.Server -import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import kotlinx.io.Sink -import kotlinx.io.Source -import kotlinx.io.asSink -import kotlinx.io.asSource -import kotlinx.io.buffered -import org.awaitility.kotlin.await -import org.junit.jupiter.api.BeforeAll -import java.io.BufferedReader -import java.io.File -import java.io.InputStreamReader -import java.net.ServerSocket -import java.net.Socket -import java.util.concurrent.TimeUnit -import kotlin.io.path.createTempDirectory -import kotlin.time.Duration.Companion.seconds - -enum class OldSchemaTransportKind { SSE, STDIO, DEFAULT } - -@Retry(times = 3) -abstract class OldSchemaTsTestBase { - - protected open val transportKind: OldSchemaTransportKind = OldSchemaTransportKind.DEFAULT - - protected val projectRoot: File get() = File(System.getProperty("user.dir")) - protected val tsClientDir: File - get() { - val base = File( - projectRoot, - "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript", - ) - - // Allow override via system property for CI: -Dts.transport=stdio|sse - val fromProp = System.getProperty("ts.transport")?.lowercase() - val overrideSubDir = when (fromProp) { - "stdio" -> "stdio" - "sse" -> "sse" - else -> null - } - - val subDirName = overrideSubDir ?: when (transportKind) { - OldSchemaTransportKind.STDIO -> "stdio" - OldSchemaTransportKind.SSE -> "sse" - OldSchemaTransportKind.DEFAULT -> null - } - if (subDirName != null) { - val sub = File(base, subDirName) - if (sub.exists()) return sub - } - return base - } - - companion object { - @JvmStatic - private val tempRootDir: File = createTempDirectory("typescript-sdk-").toFile().apply { deleteOnExit() } - - @JvmStatic - protected val sdkDir: File = File(tempRootDir, "typescript-sdk") - - @JvmStatic - @BeforeAll - fun setupTypeScriptSdk() { - println("Cloning TypeScript SDK repository") - - if (!sdkDir.exists()) { - val process = ProcessBuilder( - "git", - "clone", - "--depth", - "1", - "https://github.com/modelcontextprotocol/typescript-sdk.git", - sdkDir.absolutePath, - ) - .redirectErrorStream(true) - .start() - val exitCode = process.waitFor() - if (exitCode != 0) { - throw RuntimeException("Failed to clone TypeScript SDK repository: exit code $exitCode") - } - } - - println("Installing TypeScript SDK dependencies") - executeCommand("npm install", sdkDir, allowFailure = false, timeoutSeconds = null) - } - - @JvmStatic - protected fun killProcessOnPort(port: Int) { - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val killCommand = if (isWindows) { - "netstat -ano | findstr :$port | for /f \"tokens=5\" %a in ('more')" + - " do taskkill /F /PID %a 2>nul || echo No process found" - } else { - "lsof -ti:$port | xargs kill -9 2>/dev/null || true" - } - executeCommand(killCommand, File("."), allowFailure = true, timeoutSeconds = null) - } - - @JvmStatic - protected fun findFreePort(): Int { - ServerSocket(0).use { socket -> - return socket.localPort - } - } - - @JvmStatic - protected fun executeCommand( - command: String, - workingDir: File, - allowFailure: Boolean = false, - timeoutSeconds: Long? = null, - ): String { - if (!workingDir.exists()) { - if (!workingDir.mkdirs()) { - throw RuntimeException("Failed to create working directory: ${workingDir.absolutePath}") - } - } - - if (!workingDir.isDirectory || !workingDir.canRead()) { - throw RuntimeException("Working directory is not accessible: ${workingDir.absolutePath}") - } - - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val processBuilder = if (isWindows) { - ProcessBuilder() - .command("cmd.exe", "/c", "set TYPESCRIPT_SDK_DIR=${sdkDir.absolutePath} && $command") - } else { - ProcessBuilder() - .command("bash", "-c", "TYPESCRIPT_SDK_DIR='${sdkDir.absolutePath}' $command") - } - - val process = processBuilder - .directory(workingDir) - .redirectErrorStream(true) - .start() - - val output = StringBuilder() - BufferedReader(InputStreamReader(process.inputStream)).use { reader -> - var line: String? - while (reader.readLine().also { line = it } != null) { - println(line) - output.append(line).append("\n") - } - } - - if (timeoutSeconds == null) { - val exitCode = process.waitFor() - if (!allowFailure && exitCode != 0) { - throw RuntimeException( - "Command execution failed with exit code $exitCode: $command\n" + - "Working dir: ${workingDir.absolutePath}\nOutput:\n$output", - ) - } - } else { - process.waitFor(timeoutSeconds, TimeUnit.SECONDS) - } - - return output.toString() - } - } - - private fun waitForProcessTermination(process: Process, timeoutSeconds: Long): Boolean { - if (process.isAlive && !process.waitFor(timeoutSeconds, TimeUnit.SECONDS)) { - process.destroyForcibly() - process.waitFor(2, TimeUnit.SECONDS) - return false - } - return true - } - - private fun createProcessOutputReader(process: Process, prefix: String = "TS-SERVER"): Thread { - val outputReader = Thread { - try { - process.inputStream.bufferedReader().useLines { lines -> - for (line in lines) { - println("[$prefix] $line") - } - } - } catch (e: Exception) { - println("Warning: Error reading process output: ${e.message}") - } - } - outputReader.isDaemon = true - return outputReader - } - - private fun createProcessErrorReader(process: Process, prefix: String = "TS-SERVER"): Thread { - val errorReader = Thread { - try { - process.errorStream.bufferedReader().useLines { lines -> - for (line in lines) { - println("[$prefix][err] $line") - } - } - } catch (e: Exception) { - println("Warning: Error reading process error stream: ${e.message}") - } - } - errorReader.isDaemon = true - return errorReader - } - - protected fun waitForPort(host: String = "localhost", port: Int, timeoutSeconds: Long = 10): Boolean = try { - await.atMost(timeoutSeconds, TimeUnit.SECONDS) - .pollDelay(200, TimeUnit.MILLISECONDS) - .pollInterval(100, TimeUnit.MILLISECONDS) - .until { - try { - Socket(host, port).use { true } - } catch (_: Exception) { - false - } - } - true - } catch (_: Exception) { - false - } - - protected fun executeCommandAllowingFailure(command: String, workingDir: File, timeoutSeconds: Long = 20): String = - executeCommand(command, workingDir, allowFailure = true, timeoutSeconds = timeoutSeconds) - - protected fun startTypeScriptServer(port: Int): Process { - killProcessOnPort(port) - - if (!sdkDir.exists() || !sdkDir.isDirectory) { - throw IllegalStateException( - "TypeScript SDK directory does not exist or is not accessible: ${sdkDir.absolutePath}", - ) - } - - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val localServerPath = File(tsClientDir, "simpleStreamableHttp.ts").absolutePath - val processBuilder = if (isWindows) { - ProcessBuilder() - .command( - "cmd.exe", - "/c", - "set MCP_PORT=$port && set NODE_PATH=${sdkDir.absolutePath}\\node_modules && npx --prefix \"${sdkDir.absolutePath}\" tsx \"$localServerPath\"", - ) - } else { - ProcessBuilder() - .command( - "bash", - "-c", - "MCP_PORT=$port NODE_PATH='${sdkDir.absolutePath}/node_modules' npx --prefix '${sdkDir.absolutePath}' tsx \"$localServerPath\"", - ) - } - - processBuilder.environment()["TYPESCRIPT_SDK_DIR"] = sdkDir.absolutePath - - val process = processBuilder - .directory(tsClientDir) - .redirectErrorStream(true) - .start() - - createProcessOutputReader(process).start() - - if (!waitForPort(port = port, timeoutSeconds = 20)) { - throw IllegalStateException("TypeScript server did not become ready on localhost:$port within timeout") - } - return process - } - - protected fun stopProcess(process: Process, waitSeconds: Long = 3, name: String = "TypeScript server") { - process.destroy() - if (waitForProcessTermination(process, waitSeconds)) { - println("$name stopped gracefully") - } else { - println("$name did not stop gracefully, forced termination") - } - } - - // ===== SSE client helpers ===== - protected suspend fun newClient(serverUrl: String): Client = - HttpClient(CIO) { install(SSE) }.mcpStreamableHttp(serverUrl) - - protected suspend fun withClient(serverUrl: String, block: suspend (Client) -> T): T { - val client = newClient(serverUrl) - return try { - withTimeout(20.seconds) { block(client) } - } finally { - try { - withTimeout(3.seconds) { client.close() } - } catch (_: Exception) { - // ignore errors - } - } - } - - // ===== STDIO client + server helpers ===== - protected fun startTypeScriptServerStdio(): Process { - if (!sdkDir.exists() || !sdkDir.isDirectory) { - throw IllegalStateException( - "TypeScript SDK directory does not exist or is not accessible: ${sdkDir.absolutePath}", - ) - } - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val localServerPath = File(tsClientDir, "simpleStdio.ts").absolutePath - val processBuilder = if (isWindows) { - ProcessBuilder() - .command( - "cmd.exe", - "/c", - "set NODE_PATH=${sdkDir.absolutePath}\\node_modules && npx --prefix \"${sdkDir.absolutePath}\" tsx \"$localServerPath\"", - ) - } else { - ProcessBuilder() - .command( - "bash", - "-c", - "NODE_PATH='${sdkDir.absolutePath}/node_modules' npx --prefix '${sdkDir.absolutePath}' tsx \"$localServerPath\"", - ) - } - processBuilder.environment()["TYPESCRIPT_SDK_DIR"] = sdkDir.absolutePath - val process = processBuilder - .directory(tsClientDir) - .redirectErrorStream(false) - .start() - // For stdio transports, do NOT read from stdout (it's used for protocol). Read stderr for logs only. - createProcessErrorReader(process, prefix = "TS-SERVER-STDIO").start() - // Give the process a moment to start - await.atMost(2, TimeUnit.SECONDS) - .pollDelay(200, TimeUnit.MILLISECONDS) - .pollInterval(100, TimeUnit.MILLISECONDS) - .until { process.isAlive } - return process - } - - protected suspend fun newClientStdio(process: Process): Client { - val input: Source = process.inputStream.asSource().buffered() - val output: Sink = process.outputStream.asSink().buffered() - val transport = StdioClientTransport(input = input, output = output) - val client = Client(Implementation("test", "1.0")) - client.connect(transport) - return client - } - - protected suspend fun withClientStdio(block: suspend (Client, Process) -> T): T { - val proc = startTypeScriptServerStdio() - val client = newClientStdio(proc) - return try { - withTimeout(20.seconds) { block(client, proc) } - } finally { - try { - withTimeout(3.seconds) { client.close() } - } catch (_: Exception) { - } - try { - stopProcess(proc, name = "TypeScript stdio server") - } catch (_: Exception) { - } - } - } - - // ===== Helpers to run TypeScript client over STDIO against Kotlin server over STDIO ===== - protected fun runStdioClient(vararg args: String): String { - // Start Node stdio client (it will speak MCP over its stdout/stdin) - val isWindows = System.getProperty("os.name").lowercase().contains("windows") - val clientPath = File(tsClientDir, "myClient.ts").absolutePath - - val process = if (isWindows) { - ProcessBuilder() - .command( - "cmd.exe", - "/c", - ( - "set TYPESCRIPT_SDK_DIR=${sdkDir.absolutePath} && " + - "set NODE_PATH=${sdkDir.absolutePath}\\node_modules && " + - "npx --prefix \"${sdkDir.absolutePath}\" tsx \"$clientPath\" " + - args.joinToString(" ") - ), - ) - .directory(tsClientDir) - .redirectErrorStream(false) - .start() - } else { - ProcessBuilder() - .command( - "bash", - "-c", - ( - "TYPESCRIPT_SDK_DIR='${sdkDir.absolutePath}' " + - "NODE_PATH='${sdkDir.absolutePath}/node_modules' " + - "npx --prefix '${sdkDir.absolutePath}' tsx \"$clientPath\" " + - args.joinToString(" ") - ), - ) - .directory(tsClientDir) - .redirectErrorStream(false) - .start() - } - - // Create Kotlin server and attach stdio transport to the process streams - val server: Server = KotlinServerForTsClient().createMcpServer() - val transport = StdioServerTransport( - inputStream = process.inputStream.asSource().buffered(), - outputStream = process.outputStream.asSink().buffered(), - ) - - // Connect server in a background thread to avoid blocking - val serverThread = Thread { - try { - runBlocking { server.createSession(transport) } - } catch (e: Exception) { - println("[STDIO-SERVER] Error connecting: ${e.message}") - } - } - serverThread.isDaemon = true - serverThread.start() - - // Read ONLY stderr from client for human-readable output - val output = StringBuilder() - val errReader = Thread { - try { - process.errorStream.bufferedReader().useLines { lines -> - lines.forEach { line -> - println("[TS-CLIENT-STDIO][err] $line") - output.append(line).append('\n') - } - } - } catch (e: Exception) { - println("Warning: Error reading stdio client stderr: ${e.message}") - } - } - errReader.isDaemon = true - errReader.start() - - // Wait up to 25s for client to exit - val finished = process.waitFor(25, TimeUnit.SECONDS) - if (!finished) { - println("Stdio client did not finish in time; destroying") - process.destroyForcibly() - } - - try { - runBlocking { transport.close() } - } catch (_: Exception) { - } - - return output.toString() - } -} diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt index 54c6e0f..ff00bf9 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerEdgeCasesTestSse.kt @@ -3,6 +3,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -22,6 +23,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds +@DisabledOnCI class KotlinClientTsServerEdgeCasesTestSse : TsTestBase() { override val transportKind = TransportKind.SSE diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt index 95a80f0..b9a7b5d 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt @@ -3,11 +3,13 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerTest import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI import kotlinx.coroutines.withTimeout import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import kotlin.time.Duration.Companion.seconds +@DisabledOnCI class KotlinClientTsServerTestSse : AbstractKotlinClientTsServerTest() { override val transportKind = TransportKind.SSE diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinClientTsServerEdgeCasesTestSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinClientTsServerEdgeCasesTestSse.kt index 5af315b..c2a4529 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinClientTsServerEdgeCasesTestSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinClientTsServerEdgeCasesTestSse.kt @@ -2,8 +2,9 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.OldSchemaTransportKind -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.OldSchemaTsTestBase +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -22,9 +23,10 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds -class OldSchemaKotlinClientTsServerEdgeCasesTestSse : OldSchemaTsTestBase() { +@DisabledOnCI +class OldSchemaKotlinClientTsServerEdgeCasesTestSse : TsTestBase() { - override val transportKind = OldSchemaTransportKind.SSE + override val transportKind = TransportKind.SSE private var port: Int = 0 private val host = "localhost" diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinClientTsServerTestSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinClientTsServerTestSse.kt index 39ac18c..e0990ee 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinClientTsServerTestSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinClientTsServerTestSse.kt @@ -2,15 +2,17 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.integration.typescript.OldSchemaAbstractKotlinClientTsServerTest -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.OldSchemaTransportKind +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI import kotlinx.coroutines.withTimeout import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import kotlin.time.Duration.Companion.seconds +@DisabledOnCI class OldSchemaKotlinClientTsServerTestSse : OldSchemaAbstractKotlinClientTsServerTest() { - override val transportKind = OldSchemaTransportKind.SSE + override val transportKind = TransportKind.SSE private var port: Int = 0 private val host = "localhost" diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt index edee427..e95db0e 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt @@ -2,9 +2,11 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractTsClientKotlinServerTest import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach +@DisabledOnCI class TsClientKotlinServerTestSse : AbstractTsClientKotlinServerTest() { override val transportKind = TransportKind.SSE diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt index 65b003d..0ba11de 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsEdgeCasesTestSse.kt @@ -2,6 +2,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -18,6 +19,7 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue import kotlin.test.fail +@DisabledOnCI class TsEdgeCasesTestSse : TsTestBase() { override val transportKind = TransportKind.SSE diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt index 2bdd361..7abe0cb 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerEdgeCasesTestStdio.kt @@ -3,6 +3,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -18,6 +19,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue +@DisabledOnCI class KotlinClientTsServerEdgeCasesTestStdio : TsTestBase() { override val transportKind = TransportKind.STDIO diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerTestStdio.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerTestStdio.kt index 8273724..61f23c4 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerTestStdio.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerTestStdio.kt @@ -3,7 +3,9 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerTest import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI +@DisabledOnCI class KotlinClientTsServerTestStdio : AbstractKotlinClientTsServerTest() { override val transportKind = TransportKind.STDIO diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/OldSchemaKotlinClientTsServerEdgeCasesTestStdio.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/OldSchemaKotlinClientTsServerEdgeCasesTestStdio.kt index 3666622..ae2e09f 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/OldSchemaKotlinClientTsServerEdgeCasesTestStdio.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/OldSchemaKotlinClientTsServerEdgeCasesTestStdio.kt @@ -2,8 +2,9 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.OldSchemaTransportKind -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.OldSchemaTsTestBase +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -18,9 +19,10 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue -class OldSchemaKotlinClientTsServerEdgeCasesTestStdio : OldSchemaTsTestBase() { +@DisabledOnCI +class OldSchemaKotlinClientTsServerEdgeCasesTestStdio : TsTestBase() { - override val transportKind = OldSchemaTransportKind.STDIO + override val transportKind = TransportKind.STDIO @Test @Timeout(30, unit = TimeUnit.SECONDS) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/OldSchemaKotlinClientTsServerTestStdio.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/OldSchemaKotlinClientTsServerTestStdio.kt index b1b5bae..7daac4a 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/OldSchemaKotlinClientTsServerTestStdio.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/OldSchemaKotlinClientTsServerTestStdio.kt @@ -2,11 +2,13 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.integration.typescript.OldSchemaAbstractKotlinClientTsServerTest -import io.modelcontextprotocol.kotlin.sdk.integration.typescript.OldSchemaTransportKind +import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI +@DisabledOnCI class OldSchemaKotlinClientTsServerTestStdio : OldSchemaAbstractKotlinClientTsServerTest() { - override val transportKind = OldSchemaTransportKind.STDIO + override val transportKind = TransportKind.STDIO override suspend fun useClient(block: suspend (Client) -> T): T = withClientStdio { client, proc -> try { diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsClientKotlinServerTestStdio.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsClientKotlinServerTestStdio.kt index abf821b..05f55bc 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsClientKotlinServerTestStdio.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsClientKotlinServerTestStdio.kt @@ -2,7 +2,9 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractTsClientKotlinServerTest import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI +@DisabledOnCI class TsClientKotlinServerTestStdio : AbstractTsClientKotlinServerTest() { override val transportKind = TransportKind.STDIO override fun runClient(vararg args: String): String = runStdioClient(*args) diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsEdgeCasesTestStdio.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsEdgeCasesTestStdio.kt index dafda6d..cd1fa4d 100644 --- a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsEdgeCasesTestStdio.kt +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsEdgeCasesTestStdio.kt @@ -2,6 +2,7 @@ package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TsTestBase +import io.modelcontextprotocol.kotlin.sdk.integration.utils.DisabledOnCI import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout import org.junit.jupiter.api.condition.EnabledOnOs @@ -11,6 +12,7 @@ import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertTrue +@DisabledOnCI class TsEdgeCasesTestStdio : TsTestBase() { override val transportKind: TransportKind = TransportKind.STDIO diff --git a/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/DisabledOnCI.kt b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/DisabledOnCI.kt new file mode 100644 index 0000000..087d201 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils/DisabledOnCI.kt @@ -0,0 +1,6 @@ +package io.modelcontextprotocol.kotlin.sdk.integration.utils + +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable + +@DisabledIfEnvironmentVariable(named = "CI", matches = "true") +annotation class DisabledOnCI diff --git a/kotlin-sdk-test/src/jvmTest/resources/simplelogger.properties b/kotlin-sdk-test/src/jvmTest/resources/simplelogger.properties new file mode 100644 index 0000000..95fb646 --- /dev/null +++ b/kotlin-sdk-test/src/jvmTest/resources/simplelogger.properties @@ -0,0 +1,8 @@ +# Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) +org.slf4j.simpleLogger.defaultLogLevel=INFO +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showDateTime=false + +# Log level for specific packages or classes +org.slf4j.simpleLogger.log.io.ktor.server=DEBUG +org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG